{
  "info": {
    "name": "MalcolmAI Public API — Smoke Tests",
    "description": "Focused smoke tests for the MalcolmAI customer-facing Public API (/api/public/v1/*).\n\nAuth: collection-level Bearer using the {{apiKey}} environment variable (a full mai_<keyId>.<secret> key). Set {{baseUrl}} + {{apiKey}} in the environment before running.\n\nRun with: Collection Runner, `newman run` in CI, or a Postman Monitor.\n\nAdd-on note: /inventory, /history, /bookings, /customers and /webhooks/events require add-ons/scopes (Yard Inventory, webhooks:manage). If the key's tenant lacks them the API returns 403 — these requests treat 200-or-403 as a pass and only validate the body on 200, so the smoke run stays green on a base-scope key. Source of truth: docs/public-api/openapi.yaml.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [{ "key": "token", "value": "{{apiKey}}", "type": "string" }]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "if (!pm.environment.get('apiKey') && !pm.collectionVariables.get('apiKey')) {",
          "  console.warn('apiKey is not set — every request will 401. Set it in the environment (mai_<keyId>.<secret>).');",
          "}"
        ]
      }
    }
  ],
  "variable": [
    { "key": "testCustomerCode", "value": "POSTMAN-SMOKE", "type": "string" },
    { "key": "testDriverCdl", "value": "POSTMANSMOKE1", "type": "string" },
    { "key": "containerId", "value": "", "type": "string" },
    { "key": "containerNumber", "value": "", "type": "string" },
    { "key": "bookingId", "value": "", "type": "string" },
    { "key": "driverCdl", "value": "", "type": "string" }
  ],
  "item": [
    {
      "name": "Auth & sanity",
      "item": [
        {
          "name": "List containers (sanity + capture ids)",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/containers?limit=1",
              "host": ["{{baseUrl}}"],
              "path": ["containers"],
              "query": [{ "key": "limit", "value": "1" }]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "pm.test('response is JSON', () => pm.response.to.be.json);",
                  "const b = pm.response.json();",
                  "pm.test('data is an array', () => pm.expect(b.data).to.be.an('array'));",
                  "pm.test('pagination envelope present', () => pm.expect(b.pagination).to.include.keys(['page','limit','totalCount','totalPages']));",
                  "if (Array.isArray(b.data) && b.data.length) {",
                  "  const c = b.data[0];",
                  "  pm.collectionVariables.set('containerId', c.id);",
                  "  pm.collectionVariables.set('containerNumber', c.containerNumber);",
                  "  pm.test('container has an id', () => pm.expect(c.id).to.be.a('string').and.not.empty);",
                  "  console.log('Captured containerId=' + c.id + ' containerNumber=' + c.containerNumber);",
                  "} else {",
                  "  console.log('No containers returned — the container-detail and lookup tests will skip.');",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Rejects an invalid key (401)",
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [{ "key": "token", "value": "mai_invalid.key", "type": "string" }]
            },
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/containers?limit=1",
              "host": ["{{baseUrl}}"],
              "path": ["containers"],
              "query": [{ "key": "limit", "value": "1" }]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('401 for a bad key', () => pm.response.to.have.status(401));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Containers",
      "item": [
        {
          "name": "Get one container by id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/containers/{{containerId}}",
              "host": ["{{baseUrl}}"],
              "path": ["containers", "{{containerId}}"]
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('containerId')) {",
                  "  console.log('No containerId captured; skipping.');",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "const b = pm.response.json();",
                  "pm.test('returns the requested container', () => pm.expect(b.data.id).to.eql(pm.collectionVariables.get('containerId')));"
                ]
              }
            }
          ]
        },
        {
          "name": "Exact container-number lookup",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/containers?containerNumber={{containerNumber}}",
              "host": ["{{baseUrl}}"],
              "path": ["containers"],
              "query": [{ "key": "containerNumber", "value": "{{containerNumber}}" }]
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('containerNumber')) {",
                  "  console.log('No containerNumber captured; skipping.');",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "const b = pm.response.json();",
                  "pm.test('data is an array', () => pm.expect(b.data).to.be.an('array'));",
                  "pm.test('every row matches the requested number', () => b.data.forEach(c => pm.expect(c.containerNumber).to.eql(pm.collectionVariables.get('containerNumber'))));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Yard",
      "item": [
        {
          "name": "On-yard inventory",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/inventory?limit=5",
              "host": ["{{baseUrl}}"],
              "path": ["inventory"],
              "query": [{ "key": "limit", "value": "5" }]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200, or 403 if Yard Inventory add-on is off', () => pm.expect([200, 403]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) {",
                  "  const b = pm.response.json();",
                  "  pm.test('data is an array', () => pm.expect(b.data).to.be.an('array'));",
                  "  pm.test('pagination present', () => pm.expect(b.pagination).to.be.an('object'));",
                  "} else {",
                  "  console.log('inventory 403 — Yard Inventory add-on or read:inventory scope not enabled for this key.');",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Closed visit history",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/history?limit=5",
              "host": ["{{baseUrl}}"],
              "path": ["history"],
              "query": [{ "key": "limit", "value": "5" }]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200, or 403 if add-on off', () => pm.expect([200, 403]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) {",
                  "  const b = pm.response.json();",
                  "  pm.test('data is an array', () => pm.expect(b.data).to.be.an('array'));",
                  "  pm.test('pagination present', () => pm.expect(b.pagination).to.include.keys(['totalCount']));",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Bookings",
      "item": [
        {
          "name": "List bookings (capture id)",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/bookings?limit=5",
              "host": ["{{baseUrl}}"],
              "path": ["bookings"],
              "query": [{ "key": "limit", "value": "5" }]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200, or 403 if add-on off', () => pm.expect([200, 403]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) {",
                  "  const b = pm.response.json();",
                  "  pm.test('data is an array', () => pm.expect(b.data).to.be.an('array'));",
                  "  if (b.data.length) {",
                  "    pm.collectionVariables.set('bookingId', b.data[0].id);",
                  "    console.log('Captured bookingId=' + b.data[0].id);",
                  "  } else {",
                  "    console.log('No bookings — the booking-detail test will skip.');",
                  "  }",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Get one booking by id",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/bookings/{{bookingId}}",
              "host": ["{{baseUrl}}"],
              "path": ["bookings", "{{bookingId}}"]
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('bookingId')) {",
                  "  console.log('No bookingId captured; skipping.');",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200, or 403 if add-on off', () => pm.expect([200, 403]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) {",
                  "  const b = pm.response.json();",
                  "  pm.test('returns the requested booking', () => pm.expect(b.data.id).to.eql(pm.collectionVariables.get('bookingId')));",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Customers",
      "item": [
        {
          "name": "List customers",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/customers?limit=5",
              "host": ["{{baseUrl}}"],
              "path": ["customers"],
              "query": [{ "key": "limit", "value": "5" }]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200, or 403 if add-on off', () => pm.expect([200, 403]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) {",
                  "  const b = pm.response.json();",
                  "  pm.test('data is an array', () => pm.expect(b.data).to.be.an('array'));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Upsert a customer by code (idempotent)",
          "request": {
            "method": "PUT",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Postman Smoke Test Co\",\n  \"accountNumber\": \"SMOKE-001\",\n  \"contactEmail\": \"smoke@example.com\",\n  \"paymentTerms\": \"net_30\",\n  \"status\": \"active\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/customers/{{testCustomerCode}}",
              "host": ["{{baseUrl}}"],
              "path": ["customers", "{{testCustomerCode}}"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200, or 403 if write:customers / Yard Inventory add-on off', () => pm.expect([200, 201, 403]).to.include(pm.response.code));",
                  "if (pm.response.code !== 403) {",
                  "  const b = pm.response.json();",
                  "  pm.test('outcome is created or updated', () => pm.expect(['created','updated']).to.include(b.outcome));",
                  "  pm.test('echoes the name back', () => pm.expect(b.data.name).to.eql('Postman Smoke Test Co'));",
                  "  console.log('Customer upsert outcome=' + b.outcome + ' (idempotent — re-run yields \"updated\").');",
                  "} else {",
                  "  console.log('customer upsert 403 — write:customers scope or Yard Inventory add-on not enabled.');",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Drivers",
      "item": [
        {
          "name": "List drivers (capture cdl)",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/drivers?limit=5",
              "host": ["{{baseUrl}}"],
              "path": ["drivers"],
              "query": [{ "key": "limit", "value": "5" }]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200, or 403 if read:drivers scope off', () => pm.expect([200, 403]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) {",
                  "  const b = pm.response.json();",
                  "  pm.test('data is an array', () => pm.expect(b.data).to.be.an('array'));",
                  "  pm.test('pagination present', () => pm.expect(b.pagination).to.include.keys(['totalCount']));",
                  "  pm.test('no license image fields leak', () => b.data.forEach(d => pm.expect(d).to.not.have.any.keys(['licenseImageUrl','licenseImageId','licenseFrontUrl','licenseBackUrl'])));",
                  "  if (b.data.length && b.data[0].cdlNumber) {",
                  "    pm.collectionVariables.set('driverCdl', b.data[0].cdlNumber);",
                  "    console.log('Captured driverCdl=' + b.data[0].cdlNumber);",
                  "  } else {",
                  "    console.log('No drivers with a CDL — the driver-detail test will skip.');",
                  "  }",
                  "} else {",
                  "  console.log('drivers 403 — read:drivers scope not enabled for this key.');",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Get one driver by CDL",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/drivers/{{driverCdl}}",
              "host": ["{{baseUrl}}"],
              "path": ["drivers", "{{driverCdl}}"]
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('driverCdl')) {",
                  "  console.log('No driverCdl captured; skipping.');",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200, or 403 if scope off', () => pm.expect([200, 403]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) {",
                  "  const b = pm.response.json();",
                  "  pm.test('returns the requested driver', () => pm.expect(b.data.cdlNumber).to.eql(pm.collectionVariables.get('driverCdl')));",
                  "  pm.test('no license image fields leak', () => pm.expect(b.data).to.not.have.any.keys(['licenseImageUrl','licenseImageId','licenseFrontUrl','licenseBackUrl']));",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Upsert a driver by CDL (idempotent)",
          "request": {
            "method": "PUT",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"driverName\": \"Postman Smoke Driver\",\n  \"scacCode\": \"PSMK\",\n  \"carrierName\": \"Postman Smoke Carrier LLC\",\n  \"status\": \"active\",\n  \"licenseDetails\": {\n    \"stateProvince\": \"NJ\",\n    \"country\": \"US\",\n    \"licenseClass\": \"A\",\n    \"expirationDate\": \"2028-01-31\"\n  }\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/drivers/{{testDriverCdl}}",
              "host": ["{{baseUrl}}"],
              "path": ["drivers", "{{testDriverCdl}}"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201/200, or 403 if write:drivers scope / gate add-on off', () => pm.expect([200, 201, 403]).to.include(pm.response.code));",
                  "if (pm.response.code !== 403) {",
                  "  const b = pm.response.json();",
                  "  pm.test('outcome is created or updated', () => pm.expect(['created','updated']).to.include(b.outcome));",
                  "  pm.test('echoes the driver name back', () => pm.expect(b.data.driverName).to.eql('Postman Smoke Driver'));",
                  "  pm.test('license image fields are not returned', () => pm.expect(b.data).to.not.have.any.keys(['licenseImageUrl','licenseImageId','licenseFrontUrl','licenseBackUrl']));",
                  "  console.log('Driver upsert outcome=' + b.outcome + ' (idempotent — re-run yields \"updated\").');",
                  "} else {",
                  "  console.log('driver upsert 403 — write:drivers scope or gate add-on not enabled.');",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Webhooks",
      "item": [
        {
          "name": "Subscribable event catalog",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/webhooks/events",
              "host": ["{{baseUrl}}"],
              "path": ["webhooks", "events"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200, or 403 if webhooks:manage scope off', () => pm.expect([200, 403]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) {",
                  "  const b = pm.response.json();",
                  "  pm.test('events is a non-empty array', () => pm.expect(b.events).to.be.an('array').that.is.not.empty);",
                  "  pm.test('each event has type/group/description', () => b.events.forEach(e => pm.expect(e).to.include.keys(['type','group','description'])));",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
