{
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "exec": [
          "const CryptoJS = pm.require('npm:crypto-js@4.2.0');",
          "const version = 'reachx-partner-v1';",
          "const keyId = pm.environment.get('partner_key_id');",
          "const secret = pm.environment.get('partner_secret');",
          "const baseUrl = pm.environment.get('base_url');",
          "if (!/^rxk_[a-z0-9]{26,64}$/.test(keyId || '')) throw new Error('partner_key_id is missing or invalid');",
          "if (!secret) throw new Error('partner_secret is missing');",
          "if (!baseUrl || baseUrl === 'https://partner-api.invalid') throw new Error('replace the non-routable base_url with an approved endpoint');",
          "const parsedBase = new URL(baseUrl);",
          "if (parsedBase.protocol !== 'https:' || baseUrl.endsWith('/') || parsedBase.username || parsedBase.password || parsedBase.pathname !== '/' || parsedBase.search || parsedBase.hash) throw new Error('base_url must be an HTTPS origin without credentials, trailing slash, path, query or fragment');",
          "const method = pm.request.method;",
          "if (method !== method.toUpperCase()) throw new Error('method must be uppercase');",
          "const path = '/' + pm.request.url.path.map(part => pm.variables.replaceIn(part)).join('/');",
          "if (!/^\\/partner\\/v1\\/[A-Za-z0-9._~/-]+$/.test(path) || path.includes('//') || path.split('/').some(part => part === '.' || part === '..')) throw new Error('non-canonical partner path');",
          "const encode = value => encodeURIComponent(value).replace(/[!'()*]/g, char => '%' + char.charCodeAt(0).toString(16).toUpperCase());",
          "const lexical = (left, right) => left < right ? -1 : left > right ? 1 : 0;",
          "const seen = new Set();",
          "const wirePairs = pm.request.url.query.all().filter(item => !item.disabled).map(item => {",
          "  const key = pm.variables.replaceIn(item.key || '');",
          "  const value = pm.variables.replaceIn(item.value || '');",
          "  if (!/^[\\x21-\\x7e]+$/.test(key) || !/^[\\x21-\\x7e]+$/.test(value)) throw new Error('query keys and values must be non-empty visible ASCII in this certification kit');",
          "  if (seen.has(key)) throw new Error('repeated query key');",
          "  seen.add(key);",
          "  return [key, value];",
          "});",
          "const pairs = [...wirePairs].sort((a, b) => lexical(a[0], b[0]) || lexical(a[1], b[1]));",
          "const query = pairs.map(pair => encode(pair[0]) + '=' + encode(pair[1])).join('&');",
          "const wireQuery = wirePairs.map(pair => encode(pair[0]) + '=' + encode(pair[1])).join('&');",
          "if (query !== wireQuery) throw new Error('query is not in canonical wire order');",
          "if (CryptoJS.enc.Utf8.parse(path + (query ? '?' + query : '')).sigBytes > 1024) throw new Error('canonical target exceeds 1024 bytes');",
          "const now = new Date();",
          "pm.environment.set('observed_at', new Date(Math.floor(now.getTime() / 1000) * 1000).toISOString().replace('.000Z', 'Z'));",
          "pm.environment.set('valid_until', new Date(Math.floor(now.getTime() / 1000) * 1000 + 3600000).toISOString().replace('.000Z', 'Z'));",
          "if (pm.request.body && pm.request.body.mode === 'raw') pm.request.body.raw = pm.variables.replaceIn(pm.request.body.raw || '');",
          "const body = pm.request.body && pm.request.body.mode === 'raw' ? pm.request.body.raw : '';",
          "if (body.includes('{{')) throw new Error('request body contains an unresolved variable');",
          "if (body) {",
          "  const payload = JSON.parse(body);",
          "  const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;",
          "  for (const field of ['supplier_id', 'partner_supplier_link_id']) {",
          "    if (field in payload && !uuid.test(payload[field] || '')) throw new Error('set a canonical onboarding ' + field);",
          "  }",
          "  if ('source_system' in payload && !/^[a-z0-9][a-z0-9_-]{1,79}$/.test(payload.source_system || '')) throw new Error('source_system is outside its assigned canonical grammar');",
          "}",
          "const contentSha = CryptoJS.SHA256(CryptoJS.enc.Utf8.parse(body)).toString(CryptoJS.enc.Hex);",
          "const idempotency = pm.variables.replaceIn(pm.request.headers.get('Idempotency-Key') || '');",
          "if (method !== 'GET' && !/^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/.test(idempotency)) throw new Error('mutation requires a canonical 8-128 byte Idempotency-Key');",
          "if (method === 'GET' && idempotency) throw new Error('read requests must not carry Idempotency-Key');",
          "if (method !== 'GET' && !pm.environment.get('idempotency_prefix')) throw new Error('set a unique idempotency_prefix for this certification run');",
          "const timestamp = new Date(Math.floor(Date.now() / 1000) * 1000).toISOString().replace('.000Z', 'Z');",
          "const nonce = pm.variables.replaceIn('{{$guid}}').toLowerCase();",
          "const canonical = [version, method, path, query, contentSha, idempotency, keyId.toLowerCase(), timestamp, nonce].join('\\n');",
          "const mac = CryptoJS.HmacSHA256(canonical, secret);",
          "const signature = CryptoJS.enc.Base64.stringify(mac).replace(/=/g, '').replace(/\\+/g, '-').replace(/\\//g, '_');",
          "pm.request.headers.upsert({key: 'X-ReachX-Key-Id', value: keyId.toLowerCase()});",
          "pm.request.headers.upsert({key: 'X-ReachX-Timestamp', value: timestamp});",
          "pm.request.headers.upsert({key: 'X-ReachX-Nonce', value: nonce});",
          "pm.request.headers.upsert({key: 'X-ReachX-Content-SHA256', value: contentSha});",
          "pm.request.headers.upsert({key: 'X-ReachX-Signature', value: signature});"
        ],
        "type": "text/javascript"
      }
    }
  ],
  "info": {
    "_postman_id": "c739ed58-fbb1-4d7f-92dd-1ad22a321900",
    "description": "Dark pre-release certification kit. It can stage product, variant and offer evidence and submit absolute inventory observations. It contains no moderation, price, payment or publication operation. Reach-X remains merchant of record. The planned api.reach-x.co.za endpoint is not provisioned or activated by this kit.",
    "name": "Reach-X Partner Supply API v1 — staging kit",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('bounded expected status', function () {",
              "  pm.expect([200]).to.include(pm.response.code);",
              "});"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "name": "Read partner context",
      "request": {
        "description": "Machine-authenticated certification request. Use a fresh idempotency value for a changed command.",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "method": "GET",
        "url": {
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "partner",
            "v1",
            "context"
          ],
          "raw": "{{base_url}}/partner/v1/context"
        }
      },
      "response": []
    },
    {
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('bounded expected status', function () {",
              "  pm.expect([200,201]).to.include(pm.response.code);",
              "});",
              "if (pm.response.code >= 200 && pm.response.code < 300) {",
              "  const payload = pm.response.json();",
              "  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(payload.revision_id || '')) throw new Error('response has no canonical revision_id');",
              "  pm.environment.set(\"product_revision_id\", payload.revision_id);",
              "}"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "name": "Stage Product",
      "request": {
        "body": {
          "mode": "raw",
          "options": {
            "raw": {
              "language": "json"
            }
          },
          "raw": "{\n  \"external_id\": \"DEMO-PRODUCT-001\",\n  \"facts\": {\n    \"attributes\": {\n      \"capacity\": \"1.7 L\",\n      \"colour\": \"black\"\n    },\n    \"brand\": \"Partner Demo\",\n    \"category_reference\": \"Appliances\",\n    \"description\": \"Synthetic certification product; not for customer publication.\",\n    \"name\": \"1.7 L stainless-steel kettle\"\n  },\n  \"partner_supplier_link_id\": \"{{catalogue_supplier_link_id}}\",\n  \"resource_type\": \"product\",\n  \"source_sequence\": 1,\n  \"source_system\": \"{{source_system}}\",\n  \"supplier_id\": \"{{supplier_id}}\"\n}"
        },
        "description": "Machine-authenticated certification request. Use a fresh idempotency value for a changed command.",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{idempotency_prefix}}-stage-product-000001"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "method": "PUT",
        "url": {
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "partner",
            "v1",
            "catalogue",
            "items"
          ],
          "raw": "{{base_url}}/partner/v1/catalogue/items"
        }
      },
      "response": []
    },
    {
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('bounded expected status', function () {",
              "  pm.expect([200]).to.include(pm.response.code);",
              "});"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "name": "Submit product",
      "request": {
        "description": "Machine-authenticated certification request. Use a fresh idempotency value for a changed command.",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{idempotency_prefix}}-submit-product-000001"
          }
        ],
        "method": "POST",
        "url": {
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "partner",
            "v1",
            "catalogue",
            "revisions",
            "{{product_revision_id}}",
            "submit"
          ],
          "raw": "{{base_url}}/partner/v1/catalogue/revisions/{{product_revision_id}}/submit"
        }
      },
      "response": []
    },
    {
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('bounded expected status', function () {",
              "  pm.expect([200,201]).to.include(pm.response.code);",
              "});",
              "if (pm.response.code >= 200 && pm.response.code < 300) {",
              "  const payload = pm.response.json();",
              "  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(payload.revision_id || '')) throw new Error('response has no canonical revision_id');",
              "  pm.environment.set(\"variant_revision_id\", payload.revision_id);",
              "}"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "name": "Stage Variant",
      "request": {
        "body": {
          "mode": "raw",
          "options": {
            "raw": {
              "language": "json"
            }
          },
          "raw": "{\n  \"external_id\": \"DEMO-PRODUCT-001-BLK\",\n  \"facts\": {\n    \"barcode\": \"6000000000001\",\n    \"external_product_id\": \"DEMO-PRODUCT-001\",\n    \"options\": {\n      \"colour\": \"black\"\n    },\n    \"sku\": \"DEMO-KETTLE-BLK\",\n    \"weight_grams\": 1250\n  },\n  \"partner_supplier_link_id\": \"{{catalogue_supplier_link_id}}\",\n  \"resource_type\": \"variant\",\n  \"source_sequence\": 1,\n  \"source_system\": \"{{source_system}}\",\n  \"supplier_id\": \"{{supplier_id}}\"\n}"
        },
        "description": "Machine-authenticated certification request. Use a fresh idempotency value for a changed command.",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{idempotency_prefix}}-stage-variant-000001"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "method": "PUT",
        "url": {
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "partner",
            "v1",
            "catalogue",
            "items"
          ],
          "raw": "{{base_url}}/partner/v1/catalogue/items"
        }
      },
      "response": []
    },
    {
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('bounded expected status', function () {",
              "  pm.expect([200]).to.include(pm.response.code);",
              "});"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "name": "Submit variant",
      "request": {
        "description": "Machine-authenticated certification request. Use a fresh idempotency value for a changed command.",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{idempotency_prefix}}-submit-variant-000001"
          }
        ],
        "method": "POST",
        "url": {
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "partner",
            "v1",
            "catalogue",
            "revisions",
            "{{variant_revision_id}}",
            "submit"
          ],
          "raw": "{{base_url}}/partner/v1/catalogue/revisions/{{variant_revision_id}}/submit"
        }
      },
      "response": []
    },
    {
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('bounded expected status', function () {",
              "  pm.expect([200,201]).to.include(pm.response.code);",
              "});",
              "if (pm.response.code >= 200 && pm.response.code < 300) {",
              "  const payload = pm.response.json();",
              "  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(payload.revision_id || '')) throw new Error('response has no canonical revision_id');",
              "  pm.environment.set(\"offer_revision_id\", payload.revision_id);",
              "}"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "name": "Stage Offer",
      "request": {
        "body": {
          "mode": "raw",
          "options": {
            "raw": {
              "language": "json"
            }
          },
          "raw": "{\n  \"external_id\": \"DEMO-OFFER-001\",\n  \"facts\": {\n    \"currency_code\": \"ZAR\",\n    \"external_variant_id\": \"DEMO-PRODUCT-001-BLK\",\n    \"lead_time_days\": 2,\n    \"minimum_order_quantity\": 1,\n    \"supplier_cost_cents\": 12000,\n    \"supplier_cost_tax_mode\": \"tax_exclusive\",\n    \"supplier_sku\": \"DEMO-KETTLE-BLK\",\n    \"supplier_tax_rate_bps\": 1500\n  },\n  \"partner_supplier_link_id\": \"{{offer_supplier_link_id}}\",\n  \"resource_type\": \"offer\",\n  \"source_sequence\": 1,\n  \"source_system\": \"{{source_system}}\",\n  \"supplier_id\": \"{{supplier_id}}\"\n}"
        },
        "description": "Machine-authenticated certification request. Use a fresh idempotency value for a changed command.",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{idempotency_prefix}}-stage-offer-000001"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "method": "PUT",
        "url": {
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "partner",
            "v1",
            "offers"
          ],
          "raw": "{{base_url}}/partner/v1/offers"
        }
      },
      "response": []
    },
    {
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('bounded expected status', function () {",
              "  pm.expect([200]).to.include(pm.response.code);",
              "});"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "name": "Submit offer",
      "request": {
        "description": "Machine-authenticated certification request. Use a fresh idempotency value for a changed command.",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{idempotency_prefix}}-submit-offer-000001"
          }
        ],
        "method": "POST",
        "url": {
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "partner",
            "v1",
            "catalogue",
            "revisions",
            "{{offer_revision_id}}",
            "submit"
          ],
          "raw": "{{base_url}}/partner/v1/catalogue/revisions/{{offer_revision_id}}/submit"
        }
      },
      "response": []
    },
    {
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('bounded expected status', function () {",
              "  pm.expect([200]).to.include(pm.response.code);",
              "});"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "name": "Update Inventory",
      "request": {
        "body": {
          "mode": "raw",
          "options": {
            "raw": {
              "language": "json"
            }
          },
          "raw": "{\n  \"external_inventory_id\": \"DEMO-STOCK-001\",\n  \"observed_at\": \"{{observed_at}}\",\n  \"reported_available_quantity\": 25,\n  \"source_event_id\": \"demo-stock-event-000001\",\n  \"source_sequence\": 1,\n  \"source_system\": \"{{source_system}}\",\n  \"valid_until\": \"{{valid_until}}\"\n}"
        },
        "description": "Machine-authenticated certification request. Use a fresh idempotency value for a changed command.",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{idempotency_prefix}}-inventory-update-000001"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "method": "POST",
        "url": {
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "partner",
            "v1",
            "inventory",
            "updates"
          ],
          "raw": "{{base_url}}/partner/v1/inventory/updates"
        }
      },
      "response": []
    }
  ],
  "variable": [
    {
      "key": "kit_version",
      "value": "1.0.0-pre-release"
    },
    {
      "key": "openapi_sha256",
      "value": "ce8848dd5ed61f8ccee36daec5fc58db1b2fbbefc5723f477f4011df5eb810b8"
    }
  ]
}
