{
  "info": {
    "name": "cocoPIM — API publique v1 (REST + GraphQL)",
    "description": "Collection générée depuis la **spécification OpenAPI de cocoPIM** (`GET /api/v1/openapi.json`), elle-même produite depuis le code au démarrage de l'API : les routes, paramètres et schémas ci-dessous ne peuvent pas diverger de l'implémentation.\n\n## L'authentification : une clé, pas de connexion\n\n**Il n'y a pas d'endpoint d'authentification** — ni page de connexion, ni échange de jeton, ni OAuth. La clé API est l'identifiant, présentée à chaque appel dans `Authorization: Bearer cpk_live_<préfixe>_<secret>` (`X-Api-Key: <clé>` est toléré). Cette collection pose cet en-tête au niveau de la collection : aucune requête ne le porte en propre. Le dossier « Commencer ici » contient une requête « Vérifier ma clé API » qui nomme la cause exacte d'un refus.\n\n## Démarrer en trois minutes\n\n1. Importez le fichier d'environnement fourni à côté de cette collection, et sélectionnez-le en haut à droite.\n2. Créez une clé API dans cocoPIM : **Développeurs → Clés API**. Cochez les portées dont vous avez besoin — `catalog:read` pour lire, `catalog:write` pour créer et modifier, `catalog:delete` pour supprimer.\n3. Collez la clé dans la variable `apiKey` de l'environnement. **Elle ne s'affiche qu'une fois à la création** : si vous la perdez, il faut en générer une autre.\n4. Lancez « Lister les produits ». Vous devez recevoir une page de résultats.\n\n## Ce qu'il faut savoir avant de cliquer\n\n**L'authentification** se fait au niveau de la collection (`Authorization: Bearer {{apiKey}}`) : aucune requête ne porte d'en-tête d'authentification en propre.\n\n**L'idempotence est obligatoire sur les créations.** Chaque `POST` porte un en-tête `Idempotency-Key` valorisé à `{{$guid}}`, ce qui produit une clé neuve à chaque envoi. Pour observer le rejeu, remplacez-le par une valeur fixe : même clé et même corps renvoient la même réponse sans rien créer une seconde fois ; même clé et corps différent renvoient `idempotency_key_reused`.\n\n**Les identifiants se propagent tout seuls.** Une création réussie range l'identifiant obtenu dans une variable de collection (`productId`, `categoryId`…), que les requêtes suivantes du dossier réutilisent. Enchaînez donc créer → lire → modifier → supprimer dans l'ordre.\n\n**La pagination est par curseur, jamais par numéro de page.** Reportez `meta.next_cursor` dans le paramètre `cursor` de l'appel suivant ; `meta.has_more` dit s'il reste quelque chose.\n\n**Les erreurs suivent la RFC 9457** (`application/problem+json`) : lisez le champ `code`, qui est stable entre versions, plutôt que le texte de `detail`. Les principaux : `invalid_api_key`, `api_key_expired`, `insufficient_scope`, `not_found`, `validation_failed`, `rate_limited`, `missing_idempotency_key`.\n\n## Les limites à connaître\n\n- **20 écritures par minute et par clé.** Au-delà : `429 rate_limited`. Un import massif doit s'étaler ou passer par l'import de fichiers de l'interface.\n- **Quota mensuel par clé**, si vous en avez fixé un à la création.\n- **Pack Pro ou supérieur** pour l'API REST, **Croissance ou supérieur** pour GraphQL. En deçà : `403 PACK_FEATURE_REQUIRED`.\n\n## Les variables à renseigner\n\n`baseUrl` et `apiKey` suffisent pour lire. Pour les écritures, renseignez aussi `familyCode` (une famille existante de votre espace — une création de produit l'exige), et selon les dossiers `attributeCode`, `optionListCode`, `optionValueCode`.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_postman_id": "dbbf53774cf8fd4ffb355acdfbe116ca"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Garde-fou : un appel sans clé renvoie un 401 que l'on met dix minutes",
          "// à diagnostiquer. Autant le dire tout de suite, dans la console.",
          "if (!pm.environment.get(\"apiKey\") && !pm.collectionVariables.get(\"apiKey\")) {",
          "  console.warn(",
          "    \"[cocoPIM] La variable apiKey est vide — sélectionnez l'environnement et collez votre clé (Développeurs → Clés API).\"",
          "  );",
          "}"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "familyCode",
      "value": "",
      "description": "Code d'une famille existante de votre espace. Exigé pour créer un produit."
    },
    {
      "key": "attributeCode",
      "value": "",
      "description": "Code d'un attribut existant."
    },
    {
      "key": "optionListCode",
      "value": "",
      "description": "Code d'une liste d'options existante."
    },
    {
      "key": "optionValueCode",
      "value": "",
      "description": "Code d'une valeur d'option existante."
    },
    {
      "key": "productId",
      "value": "",
      "description": "Renseigné automatiquement à la création."
    },
    {
      "key": "categoryId",
      "value": "",
      "description": "Renseigné automatiquement à la création."
    },
    {
      "key": "attributeId",
      "value": "",
      "description": "Renseigné automatiquement à la création."
    },
    {
      "key": "familyId",
      "value": "",
      "description": "Renseigné automatiquement à la création."
    },
    {
      "key": "optionListId",
      "value": "",
      "description": "Renseigné automatiquement à la création."
    }
  ],
  "item": [
    {
      "name": "Commencer ici",
      "description": "**Il n'y a pas d'endpoint d'authentification, et il n'en manque pas un.**\n\nL'API publique de cocoPIM n'a ni page de connexion, ni échange de jeton, ni flux OAuth : la clé API **est** l'identifiant. On la présente à chaque appel dans l'en-tête `Authorization: Bearer cpk_live_<préfixe>_<secret>` (l'en-tête `X-Api-Key: <clé>` est toléré, sans le mot `Bearer`). Rien à renouveler, rien à rafraîchir.\n\nCette collection pose cet en-tête **au niveau de la collection** : il s'applique à toutes les requêtes, et aucune ne le porte en propre.\n\nNe cherchez pas non plus le code à 6 chiffres reçu par e-mail : c'est la connexion des **humains** à l'interface web, un système distinct qui ne donne aucun accès à l'API.\n\nLa clé se crée dans cocoPIM — **Développeurs → Clés API** — et ne s'affiche **qu'une fois**, à la création. Rangez-la dans un gestionnaire de mots de passe : si vous la perdez, il faut en générer une autre.",
      "item": [
        {
          "name": "Vérifier ma clé API",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/products?limit=1",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "products"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "1"
                }
              ]
            },
            "description": "Le plus petit appel authentifié possible — l'équivalent d'un « est-ce que je suis connecté ? ». Le test attaché nomme la cause exacte en cas de refus, au lieu de vous laisser avec un 401 muet."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const byCode = {",
                  "  invalid_api_key: \"Clé inconnue ou mal formée. Vérifiez la variable apiKey de l'environnement (format cpk_live_<préfixe>_<secret>).\",",
                  "  api_key_expired: \"Clé expirée : sa date d'expiration est passée. Générez-en une nouvelle.\",",
                  "  insufficient_scope: \"Clé valide, mais sans la portée catalog:read. Les portées se choisissent à la CRÉATION de la clé et ne s'ajoutent pas après coup.\",",
                  "  PACK_FEATURE_REQUIRED: \"Clé valide, mais le pack de l'organisation n'ouvre pas l'API publique (pack Pro minimum ; Croissance pour GraphQL).\",",
                  "  rate_limited: \"Trop d'appels : attendez la fenêtre indiquée par l'en-tête Retry-After.\",",
                  "  monthly_quota_exceeded: \"Quota mensuel de cette clé épuisé.\",",
                  "};",
                  "",
                  "if (pm.response.code === 200) {",
                  "  pm.test(\"Clé valide — l'API répond\", () => {",
                  "    pm.expect(pm.response.json()).to.have.property(\"data\");",
                  "  });",
                  "} else {",
                  "  const problem = pm.response.json();",
                  "  const hint = byCode[problem.code] || problem.detail;",
                  "  pm.test(\"Refus : \" + problem.code, () => pm.expect.fail(hint));",
                  "}"
                ]
              }
            }
          ],
          "response": [],
          "id": "902d6ae8a45494de4fe2f1ad3f016ddf"
        },
        {
          "name": "Spécification OpenAPI (sans clé)",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/openapi.json",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "openapi.json"
              ]
            },
            "description": "Contrat complet, généré depuis le code au démarrage de l'API — il ne peut donc pas diverger des routes. Seule route de l'API publique dispensée de clé et d'entitlement de pack."
          },
          "response": [],
          "id": "d9ef0c7ba8867d228ddfe4ef7110df56"
        }
      ],
      "id": "b57e91c480d3f0a1f887d3ce659bdb03"
    },
    {
      "name": "Produits",
      "description": "Lecture et écriture du catalogue produits. Les valeurs d'attributs vivent dans `values` — un seul modèle, pas de colonnes annexes.",
      "item": [
        {
          "name": "Lister les produits",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/products?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "products"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Curseur opaque renvoyé par la réponse précédente.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Nombre d'éléments par page. Défaut: 50, max: 100.",
                  "disabled": false
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "Filtrer par statut produit.",
                  "disabled": true
                },
                {
                  "key": "familyCode",
                  "value": "",
                  "description": "Filtrer par code de famille.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "description": "Recherche textuelle sur le SKU/nom.",
                  "disabled": true
                },
                {
                  "key": "updatedAfter",
                  "value": "",
                  "description": "Retourne uniquement les produits modifiés après cette date ISO 8601.",
                  "disabled": true
                }
              ],
              "variable": []
            },
            "description": "Lister les produits\nPagination cursor-only. Scopes requis: catalog:read.\nScope requis : `catalog:read`."
          },
          "response": [],
          "id": "649497b8b1b92dabb7a01b5273130926"
        },
        {
          "name": "Détail d'un produit",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/products/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "products",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{productId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Détail d'un produit\nScope requis : `catalog:read`."
          },
          "response": [],
          "id": "8756b4e37eed311e3a3106583c101243"
        },
        {
          "name": "Variantes d'un produit maître",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/products/:id/variants?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "products",
                ":id",
                "variants"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Curseur opaque renvoyé par la réponse précédente.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Nombre d'éléments par page. Défaut: 50, max: 100.",
                  "disabled": false
                }
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{productId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Variantes d'un produit maître\nScope requis : `catalog:read`."
          },
          "response": [],
          "id": "e7db6cfcb36912d043ede877d62f3215"
        },
        {
          "name": "Créer un produit",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Clé d'idempotence (obligatoire)"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/products",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "products"
              ],
              "query": [],
              "variable": []
            },
            "description": "Créer un produit\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sku\": \"SKU-DEMO-001\",\n  \"familyCode\": \"{{familyCode}}\",\n  \"name\": \"Libellé de démonstration\",\n  \"nameTranslations\": {\n    \"en_US\": \"Demo label\"\n  },\n  \"status\": \"DRAFT\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"201 Created\", () => pm.response.to.have.status(201));",
                  "if (pm.response.code === 201) {",
                  "  const body = pm.response.json();",
                  "  if (body.id) {",
                  "    pm.collectionVariables.set(\"productId\", body.id);",
                  "    console.log(\"productId =\", body.id);",
                  "  }",
                  "}"
                ]
              }
            }
          ],
          "id": "58e08f1506aa68843225597a151954fb"
        },
        {
          "name": "Mettre à jour un produit",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/products/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "products",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{productId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Mettre à jour un produit\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Libellé de démonstration\",\n  \"nameTranslations\": {\n    \"en_US\": \"Demo label\"\n  },\n  \"status\": \"DRAFT\",\n  \"values\": {}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "id": "cddec8194e5a19ed8120f556c63e424d"
        },
        {
          "name": "Supprimer un produit",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/products/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "products",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{productId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Supprimer un produit\nScope requis : `catalog:delete`."
          },
          "response": [],
          "id": "ff5ed0332b1056f7bc87bd0142dc8546"
        }
      ],
      "id": "6949443b9eb9efa7282a2aaebf8020c5"
    },
    {
      "name": "Catégories",
      "description": "Arborescence de catégories. `parentCode` vide = racine.",
      "item": [
        {
          "name": "Lister les catégories",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/categories?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "categories"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Curseur opaque renvoyé par la réponse précédente.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Nombre d'éléments par page. Défaut: 50, max: 100.",
                  "disabled": false
                }
              ],
              "variable": []
            },
            "description": "Lister les catégories\nScope requis : `catalog:read`."
          },
          "response": [],
          "id": "d8d9666b1ccfada29447f49241138bed"
        },
        {
          "name": "Créer une catégorie",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Clé d'idempotence (obligatoire)"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/categories",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "categories"
              ],
              "query": [],
              "variable": []
            },
            "description": "Créer une catégorie\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"demo-code\",\n  \"name\": \"Libellé de démonstration\",\n  \"description\": \"valeur\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"201 Created\", () => pm.response.to.have.status(201));",
                  "if (pm.response.code === 201) {",
                  "  const body = pm.response.json();",
                  "  if (body.id) {",
                  "    pm.collectionVariables.set(\"categoryId\", body.id);",
                  "    console.log(\"categoryId =\", body.id);",
                  "  }",
                  "}"
                ]
              }
            }
          ],
          "id": "87e2db372dd002b1ba59c39799136d0f"
        },
        {
          "name": "Détail d'une catégorie",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/categories/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "categories",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{categoryId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Détail d'une catégorie\nScope requis : `catalog:read`."
          },
          "response": [],
          "id": "8d2508158c255055c39519afddc54bb7"
        },
        {
          "name": "Mettre à jour une catégorie",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/categories/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "categories",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{categoryId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Mettre à jour une catégorie\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Libellé de démonstration\",\n  \"description\": \"valeur\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "id": "c1de24fb45fb5b944f58577187ec52e9"
        },
        {
          "name": "Supprimer une catégorie",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/categories/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "categories",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{categoryId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Supprimer une catégorie\nScope requis : `catalog:delete`."
          },
          "response": [],
          "id": "71e1ebb3a7471c6f74a06ba1f8bb51d8"
        }
      ],
      "id": "64f69709d9664feccbe8ff023c97b19a"
    },
    {
      "name": "Attributs",
      "description": "Définition des attributs (le schéma), pas leurs valeurs. 13 types ; EMAIL et URL sont des TEXT avec une expression régulière, jamais des types à part.",
      "item": [
        {
          "name": "Lister les attributs",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attributes?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attributes"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Curseur opaque renvoyé par la réponse précédente.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Nombre d'éléments par page. Défaut: 50, max: 100.",
                  "disabled": false
                }
              ],
              "variable": []
            },
            "description": "Lister les attributs\nScope requis : `catalog:read`."
          },
          "response": [],
          "id": "6532b7619d745944a8ad592632388044"
        },
        {
          "name": "Créer un attribut",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Clé d'idempotence (obligatoire)"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attributes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attributes"
              ],
              "query": [],
              "variable": []
            },
            "description": "Créer un attribut\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"demo-code\",\n  \"name\": \"Libellé de démonstration\",\n  \"groupCode\": \"valeur\",\n  \"type\": \"valeur\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"201 Created\", () => pm.response.to.have.status(201));",
                  "if (pm.response.code === 201) {",
                  "  const body = pm.response.json();",
                  "  if (body.id) {",
                  "    pm.collectionVariables.set(\"attributeId\", body.id);",
                  "    console.log(\"attributeId =\", body.id);",
                  "  }",
                  "}"
                ]
              }
            }
          ],
          "id": "42c55d826963c5764de39b9ace48c452"
        },
        {
          "name": "Mettre à jour un attribut",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attributes/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attributes",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{attributeId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Mettre à jour un attribut\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Libellé de démonstration\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "id": "88704fe0281cc5027eba09a234baf5a9"
        },
        {
          "name": "Supprimer un attribut",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attributes/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attributes",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{attributeId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Supprimer un attribut\nScope requis : `catalog:delete`."
          },
          "response": [],
          "id": "78d076142e7e22aa5dd56e525c63040f"
        }
      ],
      "id": "c2aa24fb9cc27b6ca3c42d76097c5116"
    },
    {
      "name": "Familles d'attributs",
      "description": "Gabarits de schéma : une famille regroupe les attributs qu'un produit porte. Les liens famille↔attribut s'administrent ici.",
      "item": [
        {
          "name": "Lister les familles d'attributs",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attribute-families?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attribute-families"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Curseur opaque renvoyé par la réponse précédente.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Nombre d'éléments par page. Défaut: 50, max: 100.",
                  "disabled": false
                }
              ],
              "variable": []
            },
            "description": "Lister les familles d'attributs\nScope requis : `catalog:read`."
          },
          "response": [],
          "id": "08c711ade1ac703d3ac2e18ff1e34758"
        },
        {
          "name": "Créer une famille",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Clé d'idempotence (obligatoire)"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attribute-families",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attribute-families"
              ],
              "query": [],
              "variable": []
            },
            "description": "Créer une famille\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"demo-code\",\n  \"name\": \"Libellé de démonstration\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"201 Created\", () => pm.response.to.have.status(201));",
                  "if (pm.response.code === 201) {",
                  "  const body = pm.response.json();",
                  "  if (body.id) {",
                  "    pm.collectionVariables.set(\"familyId\", body.id);",
                  "    console.log(\"familyId =\", body.id);",
                  "  }",
                  "}"
                ]
              }
            }
          ],
          "id": "61970d80e4a850d471cfd38bfa1ea7c3"
        },
        {
          "name": "Mettre à jour une famille",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attribute-families/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attribute-families",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{familyId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Mettre à jour une famille\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Libellé de démonstration\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "id": "925c2137b9106ae4627a9a8e161f2262"
        },
        {
          "name": "Supprimer une famille",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attribute-families/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attribute-families",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{familyId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Supprimer une famille\nScope requis : `catalog:delete`."
          },
          "response": [],
          "id": "b80f4f22e46058d9828ea928b9cbe769"
        },
        {
          "name": "Assigner un attribut à une famille",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Clé d'idempotence (obligatoire)"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attribute-families/:familyCode/attributes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attribute-families",
                ":familyCode",
                "attributes"
              ],
              "query": [],
              "variable": [
                {
                  "key": "familyCode",
                  "value": "{{familyCode}}",
                  "description": "Identifiant utilisé dans le chemin (familyCode)."
                }
              ]
            },
            "description": "Assigner un attribut à une famille\nIdempotent : si l'attribut est déjà assigné à la famille, renvoie 200 (no-op) au lieu de créer un doublon.\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"attributeCode\": \"{{attributeCode}}\",\n  \"labels\": {}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"201 Created\", () => pm.response.to.have.status(201));",
                  "if (pm.response.code === 201) {",
                  "  const body = pm.response.json();",
                  "  if (body.id) {",
                  "    pm.collectionVariables.set(\"familyId\", body.id);",
                  "    console.log(\"familyId =\", body.id);",
                  "  }",
                  "}"
                ]
              }
            }
          ],
          "id": "b9ee5f3c53155c03243aaadedb102745"
        },
        {
          "name": "Mettre à jour les labels d'un lien famille-attribut",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attribute-families/:familyCode/attributes/:attributeCode",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attribute-families",
                ":familyCode",
                "attributes",
                ":attributeCode"
              ],
              "query": [],
              "variable": [
                {
                  "key": "familyCode",
                  "value": "{{familyCode}}",
                  "description": "Identifiant utilisé dans le chemin (familyCode)."
                },
                {
                  "key": "attributeCode",
                  "value": "{{attributeCode}}",
                  "description": "Identifiant utilisé dans le chemin (attributeCode)."
                }
              ]
            },
            "description": "Mettre à jour les labels d'un lien famille-attribut\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"labels\": {}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "id": "30184dc5f39ea8adb780e482e9300873"
        },
        {
          "name": "Retirer un attribut d'une famille",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/attribute-families/:familyCode/attributes/:attributeCode",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "attribute-families",
                ":familyCode",
                "attributes",
                ":attributeCode"
              ],
              "query": [],
              "variable": [
                {
                  "key": "familyCode",
                  "value": "{{familyCode}}",
                  "description": "Identifiant utilisé dans le chemin (familyCode)."
                },
                {
                  "key": "attributeCode",
                  "value": "{{attributeCode}}",
                  "description": "Identifiant utilisé dans le chemin (attributeCode)."
                }
              ]
            },
            "description": "Retirer un attribut d'une famille\nScope requis : `catalog:delete`."
          },
          "response": [],
          "id": "faf41cd876bb377a72647a125cbed4bc"
        }
      ],
      "id": "3c2fa50e2d42ed73b068f0100eba3021"
    },
    {
      "name": "Listes d'options",
      "description": "Listes de valeurs réutilisables pour les attributs SELECT et MULTI_SELECT.",
      "item": [
        {
          "name": "Lister les listes d'options (master uniquement)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/option-lists?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "option-lists"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Curseur opaque renvoyé par la réponse précédente.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Nombre d'éléments par page. Défaut: 50, max: 100.",
                  "disabled": false
                }
              ],
              "variable": []
            },
            "description": "Lister les listes d'options (master uniquement)\nScope requis : `catalog:read`."
          },
          "response": [],
          "id": "dd1aa4ea91b849bb009fe3946d81fb6a"
        },
        {
          "name": "Créer une liste d'options",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Clé d'idempotence (obligatoire)"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/option-lists",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "option-lists"
              ],
              "query": [],
              "variable": []
            },
            "description": "Créer une liste d'options\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"demo-code\",\n  \"name\": \"Libellé de démonstration\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"201 Created\", () => pm.response.to.have.status(201));",
                  "if (pm.response.code === 201) {",
                  "  const body = pm.response.json();",
                  "  if (body.id) {",
                  "    pm.collectionVariables.set(\"optionListId\", body.id);",
                  "    console.log(\"optionListId =\", body.id);",
                  "  }",
                  "}"
                ]
              }
            }
          ],
          "id": "90bc16509569c272fae2a6e6b5ad144f"
        },
        {
          "name": "Valeurs d'une liste d'options par code",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/option-lists/:code/options?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "option-lists",
                ":code",
                "options"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Curseur opaque renvoyé par la réponse précédente.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Nombre d'éléments par page. Défaut: 50, max: 100.",
                  "disabled": false
                }
              ],
              "variable": [
                {
                  "key": "code",
                  "value": "{{optionListCode}}",
                  "description": "Identifiant utilisé dans le chemin (code)."
                }
              ]
            },
            "description": "Valeurs d'une liste d'options par code\nScope requis : `catalog:read`."
          },
          "response": [],
          "id": "8a88d2a522697f00dbf63116f11f1005"
        },
        {
          "name": "Mettre à jour une liste d'options",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/option-lists/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "option-lists",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{optionListId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Mettre à jour une liste d'options\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Libellé de démonstration\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "id": "82449c220d497f4d07347d373885c8ab"
        },
        {
          "name": "Supprimer une liste d'options",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/option-lists/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "option-lists",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{optionListId}}",
                  "description": "Identifiant utilisé dans le chemin (id)."
                }
              ]
            },
            "description": "Supprimer une liste d'options\nScope requis : `catalog:delete`."
          },
          "response": [],
          "id": "6ad3f4b63b334794a02b733647d74f70"
        },
        {
          "name": "Ajouter une valeur à une liste d'options",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Clé d'idempotence (obligatoire)"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/option-lists/:listCode/values",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "option-lists",
                ":listCode",
                "values"
              ],
              "query": [],
              "variable": [
                {
                  "key": "listCode",
                  "value": "{{optionListCode}}",
                  "description": "Identifiant utilisé dans le chemin (listCode)."
                }
              ]
            },
            "description": "Ajouter une valeur à une liste d'options\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"demo-code\",\n  \"labels\": {},\n  \"color\": \"valeur\",\n  \"isActive\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"201 Created\", () => pm.response.to.have.status(201));",
                  "if (pm.response.code === 201) {",
                  "  const body = pm.response.json();",
                  "  if (body.id) {",
                  "    pm.collectionVariables.set(\"optionListId\", body.id);",
                  "    console.log(\"optionListId =\", body.id);",
                  "  }",
                  "}"
                ]
              }
            }
          ],
          "id": "25754157c8e0de2c38994d30582d7152"
        },
        {
          "name": "Mettre à jour une valeur (libellés/couleur/activation)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/option-lists/:listCode/values/:valueCode",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "option-lists",
                ":listCode",
                "values",
                ":valueCode"
              ],
              "query": [],
              "variable": [
                {
                  "key": "listCode",
                  "value": "{{optionListCode}}",
                  "description": "Identifiant utilisé dans le chemin (listCode)."
                },
                {
                  "key": "valueCode",
                  "value": "{{optionValueCode}}",
                  "description": "Identifiant utilisé dans le chemin (valueCode)."
                }
              ]
            },
            "description": "Mettre à jour une valeur (libellés/couleur/activation)\nScope requis : `catalog:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"labels\": {},\n  \"color\": \"valeur\",\n  \"isActive\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "id": "9acd5d5afcaaec55ab4579c56a10f1e7"
        },
        {
          "name": "Supprimer une valeur",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/option-lists/:listCode/values/:valueCode",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "option-lists",
                ":listCode",
                "values",
                ":valueCode"
              ],
              "query": [],
              "variable": [
                {
                  "key": "listCode",
                  "value": "{{optionListCode}}",
                  "description": "Identifiant utilisé dans le chemin (listCode)."
                },
                {
                  "key": "valueCode",
                  "value": "{{optionValueCode}}",
                  "description": "Identifiant utilisé dans le chemin (valueCode)."
                }
              ]
            },
            "description": "Supprimer une valeur\nScope requis : `catalog:delete`."
          },
          "response": [],
          "id": "df8b866cc2bea1d7f4f53ece94cfd18a"
        }
      ],
      "id": "93b7e465505dc3406a159fb767385381"
    },
    {
      "name": "GraphQL",
      "description": "Point d'entrée unique `POST /api/v1/graphql`, même clé API et même quota mensuel que le REST.\n\n**Pack Croissance ou Enterprise** — en deçà, la route répond `403 PACK_FEATURE_REQUIRED`.\n\n### Deux enveloppes d'erreur, à ne pas confondre\n\nUne erreur **d'exécution** (scope manquant, validation, ressource absente) arrive en **HTTP 200**, dans `errors[].extensions.code`. Un test qui se contente de vérifier le statut 200 ne valide donc rien : les tests attachés à chaque requête contrôlent aussi `errors`.\n\nUne erreur **antérieure à l'exécution** (syntaxe, profondeur, complexité, clé invalide, quota) arrive avec son **vrai statut HTTP** et un corps `application/problem+json`, sans enveloppe `data`/`errors`.\n\n### Les garde-fous qui font échouer une requête trop ambitieuse\n\n- **Profondeur maximale 10**, **budget de complexité ~1000 points**, 50 000 nœuds, 30 fragments.\n- **Une seule opération par document**, et **un seul champ racine par `mutation`** (les `query` peuvent en porter plusieurs). Pas de batching.\n- Un champ de liste **sans `limit` explicite compte pour 50** dans le calcul de complexité : deux niveaux imbriqués sans limite dépassent le budget et sont refusés avant exécution. Donnez toujours un `limit`.\n- Exécution plafonnée à 5 s, corps à 100 Ko, profondeur JSON à 32.\n\n### Idempotence\n\nSept mutations exigent l'en-tête `Idempotency-Key` : les six `create*` et `assignFamilyAttribute`. Les `update*` et `delete*` n'en veulent pas.",
      "item": [
        {
          "name": "Schéma SDL (sans clé)",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/graphql/sdl",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "graphql",
                "sdl"
              ]
            },
            "description": "Le schéma en clair, servi sans clé API — l'équivalent GraphQL de la spécification OpenAPI. C'est la source de vérité : si une requête de cette collection diverge un jour, c'est ce fichier qui a raison."
          },
          "response": [],
          "id": "a5ced9d4693173b8c1ec81fc5fe8985f"
        },
        {
          "name": "Lectures",
          "description": "Scope `catalog:read`. Sept queries, plus l'introspection.",
          "item": [
            {
              "name": "Produits — liste filtrable",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "query Products($limit: Int, $cursor: String, $status: String, $familyCode: String, $search: String, $updatedAfter: DateTime) {\n  products(limit: $limit, cursor: $cursor, status: $status, familyCode: $familyCode, search: $search, updatedAfter: $updatedAfter) {\n    nodes {\n      id\n      sku\n      name\n      nameTranslations\n      status\n      productType\n      familyCode\n      completenessScore\n      mainImage {\n        url\n        source\n      }\n      values\n      createdAt\n      updatedAt\n    }\n    pageInfo {\n      endCursor\n      hasNextPage\n    }\n    limit\n  }\n}",
                    "variables": "{\n  \"limit\": 20,\n  \"cursor\": null,\n  \"status\": null,\n  \"familyCode\": null,\n  \"search\": null,\n  \"updatedAfter\": null\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Tous les filtres de la liste REST, en un seul appel. La pagination est par curseur : reportez `pageInfo.endCursor` dans `cursor`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "63ba09f572638ed509093153faa75ee1"
            },
            {
              "name": "Produit — fiche complète (l'intérêt de GraphQL)",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "query Product($id: ID!, $variantLimit: Int) {\n  product(id: $id) {\n    id\n    sku\n    name\n    nameTranslations\n    status\n    productType\n    familyCode\n    familyLabels\n    completenessScore\n    mainAssetId\n    mainImage {\n      url\n      source\n    }\n    values\n    createdAt\n    updatedAt\n    family {\n      id\n      code\n      name\n      description\n      attributeCodes\n      attributes {\n        code\n        labels\n      }\n    }\n    variants(limit: $variantLimit) {\n      nodes {\n        id\n        sku\n        name\n        status\n      }\n      pageInfo {\n        endCursor\n        hasNextPage\n      }\n      limit\n    }\n  }\n}",
                    "variables": "{\n  \"id\": \"{{productId}}\",\n  \"variantLimit\": 10\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Produit + image + famille + liens d'attributs + variantes en UN appel, là où le REST en demande trois ou quatre. C'est la requête qui justifie GraphQL.\n\n`variants` est une connexion : donnez-lui toujours un `limit` explicite — sans lui, le calculateur de complexité applique 50 et la requête peut être refusée avant même de s'exécuter."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "7052276eda1570879569eb7b0fe05e01"
            },
            {
              "name": "Catégories — liste",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "query Categories($limit: Int, $cursor: String) {\n  categories(limit: $limit, cursor: $cursor) {\n    nodes {\n      id\n      code\n      name\n      nameTranslations\n      parentCode\n      description\n      sortOrder\n      isActive\n      createdAt\n      updatedAt\n    }\n    pageInfo {\n      endCursor\n      hasNextPage\n    }\n    limit\n  }\n}",
                    "variables": "{\n  \"limit\": 50,\n  \"cursor\": null\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": ""
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "942e25e7d0128e8c479fffe27e337a08"
            },
            {
              "name": "Catégorie — par identifiant",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "query Category($id: ID!) {\n  category(id: $id) {\n    id\n    code\n    name\n    nameTranslations\n    parentCode\n    description\n    sortOrder\n    isActive\n    createdAt\n    updatedAt\n  }\n}",
                    "variables": "{\n  \"id\": \"{{categoryId}}\"\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": ""
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "35e8f1ba1bab868a43dc1c9a9f3871ff"
            },
            {
              "name": "Attributs — liste",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "query Attributes($limit: Int, $cursor: String) {\n  attributes(limit: $limit, cursor: $cursor) {\n    nodes {\n      id\n      code\n      name\n      nameTranslations\n      type\n      config\n      isTranslatable\n      isScopable\n      isRequired\n      attributeGroupCode\n      createdAt\n      updatedAt\n    }\n    pageInfo {\n      endCursor\n      hasNextPage\n    }\n    limit\n  }\n}",
                    "variables": "{\n  \"limit\": 50,\n  \"cursor\": null\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Le schéma des attributs, pas leurs valeurs. `config` est un scalaire JSON : sa forme dépend du `type` de l'attribut."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "0a5cb7518a731101ce8c03ce8cfb1578"
            },
            {
              "name": "Familles — liste avec leurs liens d'attributs",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "query Families($limit: Int, $cursor: String) {\n  families(limit: $limit, cursor: $cursor) {\n    nodes {\n      id\n      code\n      name\n      nameTranslations\n      description\n      attributeCodes\n      attributes {\n        code\n        labels\n      }\n      createdAt\n      updatedAt\n    }\n    pageInfo {\n      endCursor\n      hasNextPage\n    }\n    limit\n  }\n}",
                    "variables": "{\n  \"limit\": 50,\n  \"cursor\": null\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "⚠️ `attributes` est une **liste simple** (`[FamilyAttributeLink!]!`), pas une connexion : ni `nodes`, ni `pageInfo`, ni `limit`. Chaque lien porte seulement `code` et `labels` — pour le type ou le caractère obligatoire d'un attribut, interrogez `attributes`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "df563692b3e2a2eb26b39aa4ce75d3d6"
            },
            {
              "name": "Listes d'options — avec leurs valeurs",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "query OptionLists($limit: Int, $cursor: String, $valueLimit: Int) {\n  optionLists(limit: $limit, cursor: $cursor) {\n    nodes {\n      id\n      code\n      name\n      nameTranslations\n      sortMode\n      values(limit: $valueLimit) {\n        nodes {\n          code\n          labels\n          color\n          icon\n          isActive\n          sortOrder\n        }\n        pageInfo {\n          endCursor\n          hasNextPage\n        }\n        limit\n      }\n      createdAt\n      updatedAt\n    }\n    pageInfo {\n      endCursor\n      hasNextPage\n    }\n    limit\n  }\n}",
                    "variables": "{\n  \"limit\": 5,\n  \"cursor\": null,\n  \"valueLimit\": 50\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Il n'existe pas de query `optionList` au singulier : on passe par la liste. `values` est une connexion résolue par lot — seul son `limit` est honoré, son `cursor` est ignoré par le résolveur, donc ne construisez pas de pagination profonde dessus."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "f88bd25608ed049b186f642b81662035"
            },
            {
              "name": "Introspection du schéma",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "query IntrospectTypes {\n  __schema {\n    queryType {\n      name\n    }\n    mutationType {\n      name\n    }\n    types {\n      name\n      kind\n    }\n  }\n}",
                    "variables": ""
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Ce que font les générateurs de clients typés. Exige une clé valide, comme le reste. La profondeur de l'introspection est exclue du plafond de 10, mais elle compte dans le budget de complexité."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "2afc0dc225493776ea2042525638bc9b"
            }
          ],
          "id": "5125f7901b8534d85991bdf3500bf9b1"
        },
        {
          "name": "Écritures",
          "description": "Les 21 mutations du schéma. Scope `catalog:write`, sauf les suppressions et le détachement d'attribut qui exigent `catalog:delete`.",
          "item": [
            {
              "name": "Créer un produit",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Obligatoire sur cette mutation. {{$guid}} produit une clé neuve à chaque envoi — ce qui NE teste pas l'idempotence : pour l'observer, remplacez-le par une valeur fixe et renvoyez deux fois."
                  }
                ],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation CreateProduct($input: CreateProductInput!) {\n  createProduct(input: $input) {\n    id\n    sku\n  }\n}",
                    "variables": "{\n  \"input\": {\n    \"sku\": \"SKU-POSTMAN-001\",\n    \"familyCode\": \"{{familyCode}}\",\n    \"name\": \"Produit de démonstration\",\n    \"nameTranslations\": {\n      \"en_US\": \"Demo product\"\n    },\n    \"status\": \"DRAFT\"\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Le payload ne renvoie que `id` et `sku` — aucun payload de mutation ne rend la ressource complète, c'est un choix assumé pour éviter une relecture systématique.\n\n`CreateProductInput` **ne porte pas `values`** : créez le produit, puis remplissez ses valeurs avec `updateProduct`.\nScope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "20ba03e8cb5b445e3af329977a7c3351"
            },
            {
              "name": "Modifier un produit (et ses valeurs)",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation UpdateProduct($id: ID!, $input: UpdateProductInput!) {\n  updateProduct(id: $id, input: $input) {\n    id\n  }\n}",
                    "variables": "{\n  \"id\": \"{{productId}}\",\n  \"input\": {\n    \"name\": \"Produit de démonstration v2\",\n    \"status\": \"VALIDATED\",\n    \"values\": {\n      \"description\": [\n        {\n          \"locale\": \"fr_FR\",\n          \"scope\": null,\n          \"value\": \"Description de démonstration.\"\n        }\n      ]\n    }\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "`values` suit le modèle unique `{ [code]: [{ locale, scope, value }] }` — `locale: null` pour un attribut non traduisible, `scope: null` pour une valeur commune à tous les canaux.\n\nLes valeurs sont validées côté serveur contre le schéma de la famille : une valeur invalide renvoie `validation_failed` et **rien n'est écrit**, la transaction est annulée.\nScope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "5e7f1836d94835f218f6641c3aefbc69"
            },
            {
              "name": "Supprimer un produit",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation DeleteProduct($id: ID!) {\n  deleteProduct(id: $id)\n}",
                    "variables": "{\n  \"id\": \"{{productId}}\"\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Renvoie un `Boolean!` nu : pas de bloc de sélection. Scope `catalog:delete`, distinct de l'écriture — une clé qui ne fait que créer et modifier n'a pas à l'obtenir.\nScope requis : `catalog:delete`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "141dc5be3eb12c5f0e4f67190c4c702b"
            },
            {
              "name": "Créer une catégorie",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Obligatoire sur cette mutation. {{$guid}} produit une clé neuve à chaque envoi — ce qui NE teste pas l'idempotence : pour l'observer, remplacez-le par une valeur fixe et renvoyez deux fois."
                  }
                ],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation CreateCategory($input: CreateCategoryInput!) {\n  createCategory(input: $input) {\n    id\n    code\n  }\n}",
                    "variables": "{\n  \"input\": {\n    \"code\": \"cat-postman\",\n    \"name\": \"Catégorie de démonstration\",\n    \"parentCode\": null,\n    \"description\": \"Créée depuis la collection Postman.\"\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "`parentCode` vide ou absent = catégorie racine.\nScope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "d811f074c44a0f61f3fbdd5e273f9f92"
            },
            {
              "name": "Modifier une catégorie",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation UpdateCategory($id: ID!, $input: UpdateCategoryInput!) {\n  updateCategory(id: $id, input: $input) {\n    id\n  }\n}",
                    "variables": "{\n  \"id\": \"{{categoryId}}\",\n  \"input\": {\n    \"name\": \"Catégorie de démonstration v2\",\n    \"description\": \"Mise à jour.\"\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "86dcf0b52be2c3eafcefeefe8083296b"
            },
            {
              "name": "Supprimer une catégorie",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation DeleteCategory($id: ID!) {\n  deleteCategory(id: $id)\n}",
                    "variables": "{\n  \"id\": \"{{categoryId}}\"\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:delete`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "b7fa66d8a4acb50b272d94124c74c34f"
            },
            {
              "name": "Créer un attribut",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Obligatoire sur cette mutation. {{$guid}} produit une clé neuve à chaque envoi — ce qui NE teste pas l'idempotence : pour l'observer, remplacez-le par une valeur fixe et renvoyez deux fois."
                  }
                ],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation CreateAttribute($input: CreateAttributeInput!) {\n  createAttribute(input: $input) {\n    id\n    code\n  }\n}",
                    "variables": "{\n  \"input\": {\n    \"code\": \"matiere\",\n    \"name\": \"Matière\",\n    \"groupCode\": \"general\",\n    \"type\": \"TEXT\"\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "`groupCode` est **obligatoire** : un attribut appartient toujours à un groupe. `type` vaut `TEXT` par défaut ; EMAIL et URL n'existent pas comme types — ce sont des TEXT avec une expression régulière.\nScope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "00e6b17956427cccfe0bec9f761650a0"
            },
            {
              "name": "Modifier un attribut",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation UpdateAttribute($id: ID!, $input: UpdateAttributeInput!) {\n  updateAttribute(id: $id, input: $input) {\n    id\n  }\n}",
                    "variables": "{\n  \"id\": \"{{attributeId}}\",\n  \"input\": {\n    \"name\": \"Matière principale\"\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "559733e8184c7fe9d90657b19e0cb805"
            },
            {
              "name": "Supprimer un attribut",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation DeleteAttribute($id: ID!) {\n  deleteAttribute(id: $id)\n}",
                    "variables": "{\n  \"id\": \"{{attributeId}}\"\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:delete`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "b781cd3d27acf20215f46e1faec907b6"
            },
            {
              "name": "Créer une famille",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Obligatoire sur cette mutation. {{$guid}} produit une clé neuve à chaque envoi — ce qui NE teste pas l'idempotence : pour l'observer, remplacez-le par une valeur fixe et renvoyez deux fois."
                  }
                ],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation CreateFamily($input: CreateFamilyInput!) {\n  createFamily(input: $input) {\n    id\n    code\n  }\n}",
                    "variables": "{\n  \"input\": {\n    \"code\": \"demo-famille\",\n    \"name\": \"Famille de démonstration\"\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "cebb84243761a116751e03f44eb11682"
            },
            {
              "name": "Modifier une famille",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation UpdateFamily($id: ID!, $input: UpdateFamilyInput!) {\n  updateFamily(id: $id, input: $input) {\n    id\n  }\n}",
                    "variables": "{\n  \"id\": \"{{familyId}}\",\n  \"input\": {\n    \"name\": \"Famille de démonstration v2\"\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "2f96fad35dfba9bb65f7a96e6a6635af"
            },
            {
              "name": "Supprimer une famille",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation DeleteFamily($id: ID!) {\n  deleteFamily(id: $id)\n}",
                    "variables": "{\n  \"id\": \"{{familyId}}\"\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:delete`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "ce807f6f9b37c419bbfeab8d87100cfd"
            },
            {
              "name": "Associer un attribut à une famille",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Obligatoire sur cette mutation. {{$guid}} produit une clé neuve à chaque envoi — ce qui NE teste pas l'idempotence : pour l'observer, remplacez-le par une valeur fixe et renvoyez deux fois."
                  }
                ],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation AssignFamilyAttribute($familyCode: String!, $input: AssignAttributeInput!) {\n  assignFamilyAttribute(familyCode: $familyCode, input: $input) {\n    created\n    code\n    labels\n  }\n}",
                    "variables": "{\n  \"familyCode\": \"{{familyCode}}\",\n  \"input\": {\n    \"attributeCode\": \"{{attributeCode}}\",\n    \"labels\": {\n      \"fr_FR\": \"Matière\",\n      \"en_US\": \"Material\"\n    }\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Idempotent par nature : réassocier un attribut déjà lié n'est pas une erreur, la réponse porte `created: false`.\n\n`labels` est un scalaire JSON — on le demande **sans bloc de sélection**, et on l'envoie comme objet (60 clés maximum, chaque valeur ≤ 255 caractères).\nScope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "4e00e751d2e567544b4e5c87c78eb668"
            },
            {
              "name": "Modifier le libellé d'un lien famille↔attribut",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation UpdateFamilyAttributeLink($familyCode: String!, $attributeCode: String!, $input: UpdateFamilyAttributeInput!) {\n  updateFamilyAttributeLink(familyCode: $familyCode, attributeCode: $attributeCode, input: $input) {\n    code\n    labels\n  }\n}",
                    "variables": "{\n  \"familyCode\": \"{{familyCode}}\",\n  \"attributeCode\": \"{{attributeCode}}\",\n  \"input\": {\n    \"labels\": {\n      \"fr_FR\": \"Composition\"\n    }\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Le libellé d'un attribut peut différer d'une famille à l'autre — c'est précisément ce que ce lien permet.\nScope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "2ef92431ec82ac14a1bb94d1b5a45fb4"
            },
            {
              "name": "Détacher un attribut d'une famille",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation UnassignFamilyAttribute($familyCode: String!, $attributeCode: String!) {\n  unassignFamilyAttribute(familyCode: $familyCode, attributeCode: $attributeCode)\n}",
                    "variables": "{\n  \"familyCode\": \"{{familyCode}}\",\n  \"attributeCode\": \"{{attributeCode}}\"\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:delete`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "1afb8ebd49e04e7fea58cff6691c3772"
            },
            {
              "name": "Créer une liste d'options",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Obligatoire sur cette mutation. {{$guid}} produit une clé neuve à chaque envoi — ce qui NE teste pas l'idempotence : pour l'observer, remplacez-le par une valeur fixe et renvoyez deux fois."
                  }
                ],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation CreateOptionList($input: CreateOptionListInput!) {\n  createOptionList(input: $input) {\n    id\n    code\n  }\n}",
                    "variables": "{\n  \"input\": {\n    \"code\": \"couleurs\",\n    \"name\": \"Couleurs\"\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "f4877402679f8a42323e85efc96157a3"
            },
            {
              "name": "Modifier une liste d'options",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation UpdateOptionList($id: ID!, $input: UpdateOptionListInput!) {\n  updateOptionList(id: $id, input: $input) {\n    id\n  }\n}",
                    "variables": "{\n  \"id\": \"{{optionListId}}\",\n  \"input\": {\n    \"name\": \"Couleurs produit\"\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "34ab8c7bef02aca9ba6d030a75b08933"
            },
            {
              "name": "Supprimer une liste d'options",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation DeleteOptionList($id: ID!) {\n  deleteOptionList(id: $id)\n}",
                    "variables": "{\n  \"id\": \"{{optionListId}}\"\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:delete`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "2b415b5ed25cd29f7402eb626c5d6949"
            },
            {
              "name": "Ajouter une valeur à une liste d'options",
              "request": {
                "method": "POST",
                "header": [
                  {
                    "key": "Idempotency-Key",
                    "value": "{{$guid}}",
                    "description": "Obligatoire sur cette mutation. {{$guid}} produit une clé neuve à chaque envoi — ce qui NE teste pas l'idempotence : pour l'observer, remplacez-le par une valeur fixe et renvoyez deux fois."
                  }
                ],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation CreateOptionValue($listCode: String!, $input: CreateOptionValueInput!) {\n  createOptionValue(listCode: $listCode, input: $input) {\n    code\n    labels\n    color\n    isActive\n  }\n}",
                    "variables": "{\n  \"listCode\": \"{{optionListCode}}\",\n  \"input\": {\n    \"code\": \"rouge\",\n    \"labels\": {\n      \"fr_FR\": \"Rouge\",\n      \"en_US\": \"Red\"\n    },\n    \"color\": \"#E10600\",\n    \"isActive\": true\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "`labels` est ici **obligatoire** (`JSON!`). Un code déjà présent dans la liste renvoie `option_value_code_exists` (409).\nScope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "bab7642613bf495bf38e08bec6289a05"
            },
            {
              "name": "Modifier une valeur d'option",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation UpdateOptionValue($listCode: String!, $valueCode: String!, $input: UpdateOptionValueInput!) {\n  updateOptionValue(listCode: $listCode, valueCode: $valueCode, input: $input) {\n    code\n    labels\n    color\n    isActive\n  }\n}",
                    "variables": "{\n  \"listCode\": \"{{optionListCode}}\",\n  \"valueCode\": \"{{optionValueCode}}\",\n  \"input\": {\n    \"labels\": {\n      \"fr_FR\": \"Rouge vif\"\n    },\n    \"color\": \"#FF0000\",\n    \"isActive\": true\n  }\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Une valeur servant d'axe de déclinaison est verrouillée : `option_value_in_use` (409).\nScope requis : `catalog:write`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "5ee4c8cc726172f3479a6d239e1fcbbf"
            },
            {
              "name": "Supprimer une valeur d'option",
              "request": {
                "method": "POST",
                "header": [],
                "body": {
                  "mode": "graphql",
                  "graphql": {
                    "query": "mutation DeleteOptionValue($listCode: String!, $valueCode: String!) {\n  deleteOptionValue(listCode: $listCode, valueCode: $valueCode)\n}",
                    "variables": "{\n  \"listCode\": \"{{optionListCode}}\",\n  \"valueCode\": \"{{optionValueCode}}\"\n}"
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v1/graphql",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v1",
                    "graphql"
                  ]
                },
                "description": "Scope requis : `catalog:delete`."
              },
              "response": [],
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// GraphQL renvoie 200 même quand l'opération échoue : c'est errors[] qui fait foi.",
                      "// Un statut différent de 200 est une erreur ANTÉRIEURE à l'exécution, au format",
                      "// problem+json (syntaxe, profondeur, complexité, clé, quota).",
                      "if (pm.response.code !== 200) {",
                      "  const problem = pm.response.json();",
                      "  pm.test(\"refus avant exécution : \" + (problem.code || pm.response.code), () => {",
                      "    pm.expect.fail(problem.code + \" — \" + problem.detail);",
                      "  });",
                      "} else {",
                      "  const body = pm.response.json();",
                      "  pm.test(\"aucune erreur GraphQL\", () => {",
                      "    if (body.errors) {",
                      "      // Une erreur de validation de graphql-js (champ inconnu, argument manquant)",
                      "      // n'a PAS d'extensions.code : ne pas afficher \"undefined\" au lecteur.",
                      "      const first = body.errors[0];",
                      "      const code = (first.extensions || {}).code || \"erreur de validation du document\";",
                      "      pm.expect.fail(code + \" — \" + first.message);",
                      "    }",
                      "  });",
                      "",
                      "  // Propagation des identifiants créés vers les requêtes suivantes.",
                      "  const d = body.data || {};",
                      "  const keep = {",
                      "    createProduct: \"productId\",",
                      "    createCategory: \"categoryId\",",
                      "    createAttribute: \"attributeId\",",
                      "    createFamily: \"familyId\",",
                      "    createOptionList: \"optionListId\",",
                      "  };",
                      "  for (const [op, variable] of Object.entries(keep)) {",
                      "    if (d[op] && d[op].id) {",
                      "      pm.collectionVariables.set(variable, d[op].id);",
                      "      console.log(variable + \" = \" + d[op].id);",
                      "    }",
                      "  }",
                      "  if (d.createOptionValue && d.createOptionValue.code) {",
                      "    pm.collectionVariables.set(\"optionValueCode\", d.createOptionValue.code);",
                      "  }",
                      "}"
                    ]
                  }
                }
              ],
              "id": "a9d05ad2f73cbde6ac284f944914eeb4"
            }
          ],
          "id": "4bbda3480e5c810db5ef56a43fb81b61"
        }
      ],
      "id": "b6266ddf93d62aa6f8c56f6735158b68"
    }
  ]
}
