{
  "info": {
    "name": "RuleForge",
    "description": "Drive every endpoint of a running RuleForge engine — health checks, the four worked Quickstart scenarios, and admin operations. Pair with the local fixture pack (./fixtures) or a DocumentForge-backed engine.\n\nFolders:\n• Health         — liveness, bindings inspector, schema downloads.\n• Scenarios      — Level 01 (basic) → Level 04 (three-deep nested iteration).\n• Admin          — refresh source caches, mirror to DocumentForge.\n\nVariables:\n• baseUrl   http://localhost:5050\n• apiKey    (optional) X-AERO-Key header value\n\nMIT License · https://github.com/tailwind-retailing/ruleforge",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_postman_id": "ruleforge-collection-v2"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      { "key": "key", "value": "X-AERO-Key", "type": "string" },
      { "key": "value", "value": "{{apiKey}}", "type": "string" },
      { "key": "in", "value": "header", "type": "string" }
    ]
  },
  "variable": [
    { "key": "baseUrl", "value": "http://localhost:5050", "type": "string" },
    { "key": "apiKey", "value": "", "type": "string" }
  ],
  "item": [
    {
      "name": "Health",
      "description": "Liveness, bindings inspector. Run Liveness first after starting ruleforge-api.",
      "item": [
        {
          "name": "Liveness",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], "path": ["health"] },
            "description": "Returns { ok: true } when the engine has loaded its bindings and is ready to evaluate."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('engine is alive', () => pm.response.to.have.status(200));",
                  "pm.test('reports ok', () => pm.expect(pm.response.json().ok).to.eql(true));"
                ]
              }
            }
          ]
        },
        {
          "name": "Bindings",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/admin/bindings", "host": ["{{baseUrl}}"], "path": ["admin", "bindings"] },
            "description": "Returns the resolved endpoint -> ruleId@version map plus cache stats."
          }
        }
      ]
    },
    {
      "name": "Scenarios",
      "description": "The four worked Quickstart scenarios — basic to advanced. Each request body matches the matching fixture under ./fixtures/scenarios. Set ?debug=true to see the per-node trace.",
      "item": [
        {
          "name": "Level 01 · Bag policy with markup",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ancillary/bag-policy?debug=true",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "ancillary", "bag-policy"],
              "query": [{ "key": "debug", "value": "true" }]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pnr\":       \"MKP800\",\n  \"cabin\":     \"Y\",\n  \"orig\":      \"LHR\",\n  \"dest\":      \"DXB\",\n  \"route\":     \"LHR-DXB\",\n  \"bagPieces\": 3,\n  \"markup\":    0.15,\n  \"pax\":       [{ \"id\": \"p1\", \"type\": \"ADT\" }]\n}"
            },
            "description": "rule-bag-policy@7 — filter / lookup / calc. Returns { code: BAG, fee: 517.5, currency: AED, pieces: 3 }."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "pm.test('decision is apply', () => pm.expect(pm.response.json().decision).to.eql('apply'));",
                  "pm.test('result has fee', () => pm.expect(pm.response.json().result).to.have.property('fee'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Level 02 · Tier bonus eligibility",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ancillary/tier-bonus?debug=true",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "ancillary", "tier-bonus"],
              "query": [{ "key": "debug", "value": "true" }]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pnr\":   \"GLD500\",\n  \"cabin\": \"Y\",\n  \"orig\":  \"LHR\",\n  \"dest\":  \"DXB\",\n  \"pax\":   [{ \"id\": \"p1\", \"type\": \"ADT\", \"tier\": \"GOLD\" }]\n}"
            },
            "description": "rule-tier-bonus@1 — string filter on $.pax[*].tier with operator `in [GOLD, PLAT, IO]`, arraySelector `any`. Routes to a constant product on pass: { bonusPieces: 1, bonusKg: 5 }."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "pm.test('decision is apply', () => pm.expect(pm.response.json().decision).to.eql('apply'));",
                  "pm.test('result has bonusPieces', () => pm.expect(pm.response.json().result.bonusPieces).to.eql(1));"
                ]
              }
            }
          ]
        },
        {
          "name": "Level 02b · Tier bonus (skip — Blue tier)",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/ancillary/tier-bonus?debug=true",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "ancillary", "tier-bonus"],
              "query": [{ "key": "debug", "value": "true" }]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pnr\":   \"BLU200\",\n  \"cabin\": \"Y\",\n  \"orig\":  \"LHR\",\n  \"dest\":  \"DXB\",\n  \"pax\":   [{ \"id\": \"p1\", \"type\": \"ADT\", \"tier\": \"BLUE\" }]\n}"
            },
            "description": "Same rule, ineligible tier — expect decision: skip and an empty result. Demonstrates the trace for a failed filter."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('decision is skip', () => pm.expect(pm.response.json().decision).to.eql('skip'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Level 03 · Per-pax PNR tax engine",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/tax/pnr?debug=true",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "tax", "pnr"],
              "query": [{ "key": "debug", "value": "true" }]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pnr\":     \"TAX001\",\n  \"orig\":    \"LHR\",\n  \"taxCode\": \"GB1\",\n  \"pax\": [\n    { \"id\": \"p1\", \"ageCategory\": \"ADT\" },\n    { \"id\": \"p2\", \"ageCategory\": \"CHD\" }\n  ]\n}"
            },
            "description": "rule-pnr-taxes@1 — iterate $.pax, seed a tax-line shell, stamp paxId from $pax.id, lookup-replace amount from ref-tax-rates by (origin × ageCategory × code), merge with mode collect. Returns [ ADT: £26, CHD: £13 ]."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "pm.test('decision is apply', () => pm.expect(pm.response.json().decision).to.eql('apply'));",
                  "pm.test('result has 2 tax lines', () => {",
                  "  pm.expect(pm.response.json().result).to.be.an('array').with.lengthOf(2);",
                  "});",
                  "pm.test('first line has GBP amount', () => {",
                  "  pm.expect(pm.response.json().result[0].currency).to.eql('GBP');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Level 04 · Seat assignments (3-deep iteration)",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/seats/assignments?debug=true",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "seats", "assignments"],
              "query": [{ "key": "debug", "value": "true" }]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pnr\": \"SEAT001\",\n  \"journeys\": [\n    {\n      \"id\": \"j1\",\n      \"segments\": [\n        { \"id\": \"s1a\", \"flightNo\": \"EK1\", \"cabin\": \"Y\" },\n        { \"id\": \"s1b\", \"flightNo\": \"EK2\", \"cabin\": \"Y\" }\n      ]\n    },\n    {\n      \"id\": \"j2\",\n      \"segments\": [\n        { \"id\": \"s2a\", \"flightNo\": \"EK3\", \"cabin\": \"J\" }\n      ]\n    }\n  ],\n  \"pax\": [\n    { \"id\": \"p1\", \"name\": \"Alice\" },\n    { \"id\": \"p2\", \"name\": \"Bob\" }\n  ]\n}"
            },
            "description": "rule-seat-assignments@1 — three nested iterators (journeys × segments × pax) build a flat tree of seat-eligibility lines, with cabin-class names looked up from ref-cabin-class. Three closing merges collapse back through each scope."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "pm.test('decision is apply', () => pm.expect(pm.response.json().decision).to.eql('apply'));",
                  "pm.test('result is nested array', () => pm.expect(pm.response.json().result).to.be.an('array'));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Admin",
      "description": "Inspect and manage the rule estate. The /admin/* surface is gated by the same X-AERO-Key middleware as evaluation endpoints.",
      "item": [
        {
          "name": "Refresh source caches",
          "request": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/admin/refresh", "host": ["{{baseUrl}}"], "path": ["admin", "refresh"] },
            "description": "Drops the rule + reference-set source caches and re-enumerates bindings. New endpoints AND new versions are live immediately — no redeploy required."
          }
        },
        {
          "name": "Mirror local fixtures → DocumentForge",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "url": { "raw": "{{baseUrl}}/admin/mirror", "host": ["{{baseUrl}}"], "path": ["admin", "mirror"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"from\":   \"file:./fixtures\",\n  \"to\":     \"df://localhost:5000\",\n  \"apiKey\": \"your-df-key\"\n}"
            },
            "description": "Bulk-publishes every rule + reference set in the local pack to a DocumentForge instance. Idempotent — re-running is safe."
          }
        }
      ]
    }
  ]
}
