{
  "info": {
    "name": "Traceable Headless DPP Integration API (Phase 1)",
    "description": "Signed lifecycle: create -> update -> gate -> submit. Set the collection variables baseUrl / bearerToken / signingSecret (signingSecret is a secret-type var; never commit a real value). The pre-request script auto-computes the HMAC signature + a fresh Idempotency-Key for every write. The document-upload request signs over raw multipart bytes and is best driven from the Node helper in the partner guide.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    { "key": "baseUrl", "value": "https://staging.traceable.digital" },
    { "key": "bearerToken", "value": "pk_live_EXAMPLE" },
    { "key": "signingSecret", "value": "", "type": "secret" },
    { "key": "productId", "value": "" }
  ],
  "auth": {
    "type": "bearer",
    "bearer": [{ "key": "token", "value": "{{bearerToken}}" }]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Sign writes (POST/PATCH). Canonical: t.METHOD.pathWithQuery.sha256Hex(rawBody).",
          "const method = pm.request.method.toUpperCase();",
          "if (method === 'GET') { return; }",
          "const url = pm.request.url;",
          "const path = '/' + url.path.join('/');",
          "const query = url.query && url.query.count ? '?' + url.query.all().map(q => q.key + '=' + q.value).join('&') : '';",
          "const pathWithQuery = path + query;",
          "const raw = (pm.request.body && pm.request.body.mode === 'raw') ? (pm.request.body.raw || '') : '';",
          "const t = Math.floor(Date.now() / 1000);",
          "const bodyHash = CryptoJS.SHA256(raw).toString(CryptoJS.enc.Hex);",
          "const canonical = t + '.' + method + '.' + pathWithQuery + '.' + bodyHash;",
          "const v1 = CryptoJS.HmacSHA256(canonical, pm.variables.get('signingSecret')).toString(CryptoJS.enc.Hex);",
          "pm.request.headers.upsert({ key: 'X-Traceable-Signature', value: 't=' + t + ', v1=' + v1 });",
          "pm.request.headers.upsert({ key: 'Idempotency-Key', value: 'pm-' + t + '-' + Math.random().toString(36).slice(2) });",
          "// NOTE: multipart uploads must sign the RAW request bytes — use the Node helper, not this raw-body path."
        ]
      }
    }
  ],
  "item": [
    {
      "name": "1. Create draft",
      "request": {
        "method": "POST",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "url": "{{baseUrl}}/api/v1/products",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"productName\": \"VoltRide 48V Pack\",\n  \"formData\": { \"gtin\": \"09506000134352\" }\n}"
        }
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('201 Draft', () => pm.response.code === 201);",
              "const p = pm.response.json().product; if (p) pm.collectionVariables.set('productId', p.id);"
            ]
          }
        }
      ]
    },
    {
      "name": "2. Get product",
      "request": {
        "method": "GET",
        "url": "{{baseUrl}}/api/v1/products/{{productId}}"
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('200 + product', () => pm.response.code === 200 && !!pm.response.json().product);"
            ]
          }
        }
      ]
    },
    {
      "name": "3. Update fields",
      "request": {
        "method": "PATCH",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "url": "{{baseUrl}}/api/v1/products/{{productId}}",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"formData\": { \"nominal_voltage_v\": 48 },\n  \"lockVersion\": 1\n}"
        }
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('200 or 409 lock', () => [200, 409].includes(pm.response.code));"
            ]
          }
        }
      ]
    },
    {
      "name": "4. Publish gate",
      "request": {
        "method": "GET",
        "url": "{{baseUrl}}/api/v1/products/{{productId}}/gate"
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('200 gate', () => pm.response.code === 200 && 'gaps' in pm.response.json());"
            ]
          }
        }
      ]
    },
    {
      "name": "5. Submit for review",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/api/v1/products/{{productId}}/submit"
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('200 Submitted or 422 blocked', () => [200, 422].includes(pm.response.code));",
              "if (pm.response.code === 200) pm.test('status Submitted', () => pm.response.json().product.status === 'Submitted');"
            ]
          }
        }
      ]
    }
  ]
}
