{
  "info": {
    "name": "MTPL Policy Demo API — workshop",
    "description": "The analyst-workshop flow against the fully synthetic MTPL demo insurer API.\n\n**Importing:** Postman needs a signed-in (free) account to import collections — signed out it only accepts pasted cURL. No account? Use Hoppscotch (hoppscotch.io: Collections → Import → Postman → this file) or Bruno (desktop).\n\n**Setup:** open the collection variables and fill in `clientId` (analyst-01 … analyst-20 or fleet.demo) and `clientSecret` (handed out in-session). Request 02 stores the bearer token automatically; every later request uses it.\n\nFor Workshop 3, also fill in `hmacSecret` — request 10 computes the X-Signature header in its pre-request script (Postman; experimental in Hoppscotch). Shell equivalent: sig=$(printf '%s' \"$BODY\" | openssl dgst -sha256 -hmac \"$SECRET\" | awk '{print $NF}')\n\nEverything in this API is fictional by construction. Base URL: https://api.cybernotes.it/mtpl/v1",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [{ "key": "token", "value": "{{token}}", "type": "string" }]
  },
  "variable": [
    { "key": "baseUrl", "value": "https://api.cybernotes.it/mtpl/v1" },
    { "key": "clientId", "value": "analyst-01" },
    { "key": "clientSecret", "value": "" },
    { "key": "myPlate", "value": "ABC-103" },
    { "key": "hmacSecret", "value": "" },
    { "key": "token", "value": "" },
    { "key": "policyId", "value": "" }
  ],
  "item": [
    {
      "name": "01 · Coverage options (public)",
      "request": {
        "auth": { "type": "noauth" },
        "method": "GET",
        "url": "{{baseUrl}}/coverage-options",
        "description": "No auth needed — usually your first successful call. Note the RateLimit-* response headers."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "pm.test('MTPL-STD product present', () => {",
              "    pm.expect(pm.response.json().options[0].code).to.eql('MTPL-STD');",
              "});"
            ]
          }
        }
      ]
    },
    {
      "name": "02 · Get a bearer token",
      "request": {
        "auth": { "type": "noauth" },
        "method": "POST",
        "url": "{{baseUrl}}/auth/token",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"client_id\": \"{{clientId}}\",\n  \"client_secret\": \"{{clientSecret}}\"\n}"
        },
        "description": "Client-credentials flow. Fill in the collection variables `clientId` and `clientSecret` first. The token is a real HS256 JWT — paste it into jwt.io and look at sub, scope and exp. It expires after 60 minutes; just send this request again."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 OK — check clientSecret if this fails', () => pm.response.to.have.status(200));",
              "if (pm.response.code === 200) {",
              "    pm.collectionVariables.set('token', pm.response.json().access_token);",
              "    console.log('Bearer token stored in {{token}} — valid for', pm.response.json().expires_in, 'seconds');",
              "}"
            ]
          }
        }
      ]
    },
    {
      "name": "03 · Create a policy",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/policies",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"regNumber\": \"{{myPlate}}\"\n}"
        },
        "description": "Creates an active one-year MTPL policy. Change the `myPlate` variable to a plate of your own (letters/digits/dashes) — if someone in the room already insured it, you get the 409 below. regNumber and/or vin are accepted."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('201 Created (or 409 if the vehicle is taken)', () => {",
              "    pm.expect(pm.response.code).to.be.oneOf([201, 409]);",
              "});",
              "if (pm.response.code === 201) {",
              "    pm.collectionVariables.set('policyId', pm.response.json().id);",
              "    console.log('policyId stored:', pm.response.json().id);",
              "} else if (pm.response.code === 409) {",
              "    console.log('Vehicle already insured — read the problem+json body, then pick another plate.');",
              "}"
            ]
          }
        }
      ]
    },
    {
      "name": "04 · Same request again → 409 duplicate",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/policies",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"regNumber\": \"{{myPlate}}\"\n}"
        },
        "description": "One active policy per vehicle across the whole register. This is the rule the flawed v0 spec never mentions — the gap-hunt payoff."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('409 Conflict', () => pm.response.to.have.status(409));",
              "pm.test('RFC 9457 problem+json', () => {",
              "    pm.expect(pm.response.headers.get('Content-Type')).to.include('application/problem+json');",
              "    pm.expect(pm.response.json().type).to.include('/duplicate-policy');",
              "});"
            ]
          }
        }
      ]
    },
    {
      "name": "05 · List policies (pagination)",
      "request": {
        "method": "GET",
        "url": {
          "raw": "{{baseUrl}}/policies?page=1&pageSize=20",
          "host": ["{{baseUrl}}"],
          "path": ["policies"],
          "query": [
            { "key": "page", "value": "1" },
            { "key": "pageSize", "value": "20" }
          ]
        },
        "description": "You only see policies your client created. Log in as fleet.demo (change `clientId`, rerun request 02) to see 70+ policies — then page 2 stops being theoretical."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "const b = pm.response.json();",
              "console.log(`page ${b.page}/${Math.ceil(b.total / b.pageSize) || 1} — ${b.items.length} of ${b.total} policies`);"
            ]
          }
        }
      ]
    },
    {
      "name": "06 · Get one policy",
      "request": {
        "method": "GET",
        "url": "{{baseUrl}}/policies/{{policyId}}",
        "description": "Uses the policyId stored by request 03. Try a made-up id and look at the 404 problem+json shape."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": ["pm.test('200 OK', () => pm.response.to.have.status(200));"]
          }
        }
      ]
    },
    {
      "name": "07 · Update policy → status \"updated\"",
      "request": {
        "method": "PUT",
        "url": "{{baseUrl}}/policies/{{policyId}}",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"holder\": \"Demo Holder (renamed)\"\n}"
        },
        "description": "Allowed fields: regNumber, vin, holder. State machine: active → updated. Cancelled/expired policies answer 409 — they are immutable."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "pm.test('status is now \"updated\"', () => pm.expect(pm.response.json().status).to.eql('updated'));"
            ]
          }
        }
      ]
    },
    {
      "name": "08 · Cancel (send it twice — idempotent)",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/policies/{{policyId}}/cancel",
        "description": "Cancelling an already-cancelled policy returns 200 with the same representation, not an error. Send this twice and compare the responses."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 OK', () => pm.response.to.have.status(200));",
              "pm.test('status is \"cancelled\"', () => pm.expect(pm.response.json().status).to.eql('cancelled'));"
            ]
          }
        }
      ]
    },
    {
      "name": "09 · Rate limit demo (send 6× fast)",
      "request": {
        "auth": { "type": "noauth" },
        "method": "GET",
        "url": "{{baseUrl}}/limited/ping",
        "description": "This endpoint allows 5 requests per minute. Click Send six times quickly and watch RateLimit-Remaining count down to a 429 problem+json with Retry-After."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 while under the limit, 429 above it', () => {",
              "    pm.expect(pm.response.code).to.be.oneOf([200, 429]);",
              "});",
              "console.log('RateLimit-Remaining:', pm.response.headers.get('RateLimit-Remaining'),",
              "            '| Retry-After:', pm.response.headers.get('Retry-After') || '—');"
            ]
          }
        }
      ]
    },
    {
      "name": "10 · Signed policy check (Workshop 3)",
      "request": {
        "auth": { "type": "noauth" },
        "method": "POST",
        "url": "{{baseUrl}}/signed/policy-check",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "body": {
          "mode": "raw",
          "raw": "{\"regNumber\":\"ABC-101\"}"
        },
        "description": "Register lookup protected by a message signature instead of a bearer token: X-Signature = hex(HMAC-SHA256(hmacSecret, raw body)). The pre-request script computes it — fill in the `hmacSecret` collection variable first (handed out in Workshop 3).\n\nTo see the teaching 401: break the signature by editing the body after sending once, or blank the X-Signature header."
      },
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// Sign the EXACT bytes Postman will send (variables resolved first).",
              "const body = pm.variables.replaceIn(pm.request.body.raw);",
              "const secret = pm.variables.get('hmacSecret') || '';",
              "if (!secret) { console.warn('hmacSecret variable is empty — expect a 401 with the recipe.'); }",
              "const sig = CryptoJS.HmacSHA256(body, secret).toString(CryptoJS.enc.Hex);",
              "pm.request.headers.upsert({ key: 'X-Signature', value: sig });",
              "console.log('X-Signature:', sig);"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 with a valid signature', () => pm.response.to.have.status(200));",
              "if (pm.response.code === 200) {",
              "    console.log('inForce:', pm.response.json().inForce, '| policy:', pm.response.json().policyId);",
              "}"
            ]
          }
        }
      ]
    },
    {
      "name": "Host only",
      "item": [
        {
          "name": "Admin · reset seed data",
          "request": {
            "auth": { "type": "noauth" },
            "method": "POST",
            "url": "{{baseUrl}}/admin/reset",
            "header": [{ "key": "X-Admin-Token", "value": "{{adminToken}}" }],
            "description": "Workshop host only. Restores the 74-policy seed state and clears rate-limit buckets. Set the `adminToken` variable locally; never share it."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": ["pm.test('200 OK', () => pm.response.to.have.status(200));"]
              }
            }
          ]
        }
      ]
    }
  ]
}
