{
  "info": {
    "name": "Kundlistar SaaS — PDF Report Module",
    "description": "Company-facing SaaS integration only: search cities, generate a branded kundli PDF, and download it.\n\nSETUP\n1. Import this JSON into Postman.\n2. Open this collection's Variables tab. Set apiOrigin to your existing public API origin, without a trailing slash or /api path (for example https://your-api-host). The default http://localhost:5001 is for the local main API gateway.\n3. Set apiKey to your company's active API key. Keep the key private and call these APIs from your backend.\n4. Run requests 01, 02, and 03 in order. Search captures the first matching city ID when cityId is blank; inspect the results and change cityId if another city is correct. Generation captures downloadUrl and expiresAt.\n5. Allow 240 seconds for generation in Postman Settings > General > Request timeout in ms (240000). Use Send and Download on request 03 to save the PDF.\n\nREPORT OPTIONS\nEdit request 02's JSON body for the person's details, branding, notes, and color theme. Supported color.theme values: forest, saffron, indigo, burgundy, monochrome. Languages: en, hi. Chart styles: North, South, East. Ayanamshas: LAHIRI, KP. PDF sections: vedic, kp, nadi, bnn, numerology, jaimini, varshphal. Branding is optional; if supplied, name/address/phone are all required. Notes are optional plain text with title up to 100 characters and text up to 6000 characters.\n\nACCESS AND USAGE\nBoth POST requests send x-api-key. Downloads use only the token already included in downloadUrl; they do not send the company key. Generation returns a small JSON response containing downloadUrl and expiresAt, with no chart JSON or PDF base64. Reports and links expire after 10 days and can be revoked earlier. Companies default to 500 successful reports/month, shared across their keys; admins can change the quota. Months reset in Asia/Kolkata. Each successful generation consumes quota, including repeated requests. Avoid automatically retrying an ambiguous timeout because requests do not support idempotency keys. A monthly quota failure returns HTTP 429 with code REPORT_MONTHLY_QUOTA_EXCEEDED.\n\nKeep apiKey and token-bearing downloadUrl private. Clear these values before sharing or exporting your populated collection. This file includes no admin, company-console, or unrelated astrology endpoints.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      { "key": "key", "value": "x-api-key", "type": "string" },
      { "key": "value", "value": "{{apiKey}}", "type": "string" },
      { "key": "in", "value": "header", "type": "string" }
    ]
  },
  "variable": [
    { "key": "apiOrigin", "value": "http://localhost:5001", "type": "string", "description": "Existing public API origin only; no trailing slash, /api, or /saas. Local default is the main gateway." },
    { "key": "apiKey", "value": "", "type": "string", "description": "Your company's active SaaS key. Keep private; do not share populated exports." },
    { "key": "cityQuery", "value": "Delhi", "type": "string", "description": "City search text, 2–100 characters." },
    { "key": "countryCode", "value": "IN", "type": "string", "description": "Two-letter country code." },
    { "key": "cityId", "value": "", "type": "string", "description": "Selected city id from request 01. Captured from the first result only when blank; inspect the result and select the correct city." },
    { "key": "downloadUrl", "value": "", "type": "string", "description": "Private, token-bearing PDF URL captured after successful generation. Clear before sharing." },
    { "key": "expiresAt", "value": "", "type": "string", "description": "PDF link expiry captured after successful generation." }
  ],
  "item": [
    {
      "name": "01 — Search cities",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (!pm.variables.get('apiKey')) throw new Error('Set the apiKey collection variable before sending.');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('City search succeeds', function () { pm.response.to.have.status(200); });",
              "if (pm.response.code === 200) {",
              "  const data = pm.response.json();",
              "  pm.test('Response includes cities', function () { pm.expect(data.cities).to.be.an('array'); });",
              "  if (Array.isArray(data.cities) && data.cities.length && !pm.variables.get('cityId')) {",
              "    pm.collectionVariables.set('cityId', String(data.cities[0].id));",
              "  }",
              "}"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json" },
          { "key": "Accept", "value": "application/json" }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"q\": \"{{cityQuery}}\",\n  \"country\": \"{{countryCode}}\",\n  \"limit\": 10\n}",
          "options": { "raw": { "language": "json" } }
        },
        "url": "{{apiOrigin}}/api/saas/v1/cities",
        "description": "Find a database city. Requires x-api-key (inherited from collection authorization). Response: {cities:[{id,name,displayName,coordinates,...}],count}. The script fills cityId from the first result only if cityId is blank. Confirm the selected city before generating a report. limit accepts 1–50. Coordinates and historical timezone/DST are resolved on the backend."
      },
      "response": []
    },
    {
      "name": "02 — Generate branded kundli PDF",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.collectionVariables.set('downloadUrl', '');",
              "pm.collectionVariables.set('expiresAt', '');",
              "if (!pm.variables.get('apiKey')) throw new Error('Set the apiKey collection variable before sending.');",
              "if (!pm.variables.get('cityId')) throw new Error('Run city search and set cityId to the correct returned city id first.');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('Report created', function () { pm.response.to.have.status(201); });",
              "if (pm.response.code === 201) {",
              "  const data = pm.response.json();",
              "  pm.test('Response contains only the PDF link and expiry', function () {",
              "    pm.expect(data).to.have.all.keys('downloadUrl', 'expiresAt');",
              "    pm.expect(data.downloadUrl).to.be.a('string').and.match(/^https?:\\/\\/.+\\/api\\/saas\\/v1\\/reports\\/[a-f\\d]{24}\\/pdf\\?token=[a-f\\d]{64}$/i);",
              "    pm.expect(Number.isNaN(Date.parse(data.expiresAt))).to.equal(false);",
              "  });",
              "  if (typeof data.downloadUrl === 'string' && typeof data.expiresAt === 'string') {",
              "    pm.collectionVariables.set('downloadUrl', data.downloadUrl);",
              "    pm.collectionVariables.set('expiresAt', data.expiresAt);",
              "  }",
              "}"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json" },
          { "key": "Accept", "value": "application/json" }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"chart\": {\n    \"name\": \"Sample User\",\n    \"dateOfBirth\": \"1990-01-15\",\n    \"timeOfBirth\": {\n      \"hour\": 10,\n      \"minute\": 30,\n      \"second\": 0\n    },\n    \"cityId\": \"{{cityId}}\",\n    \"gender\": \"Male\",\n    \"language\": \"en\",\n    \"chartStyle\": \"North\",\n    \"ayanamshaMode\": \"LAHIRI\",\n    \"sections\": [\"vedic\", \"kp\", \"nadi\"]\n  },\n  \"branding\": {\n    \"name\": \"Your Astrology Studio\",\n    \"address\": \"12 Example Road, New Delhi, India\",\n    \"phone\": \"+91 98765 43210\"\n  },\n  \"notes\": {\n    \"title\": \"Consultation notes\",\n    \"text\": \"Prepared for your consultation. Use these insights as guidance.\"\n  },\n  \"color\": {\n    \"theme\": \"forest\"\n  }\n}",
          "options": { "raw": { "language": "json" } }
        },
        "url": "{{apiOrigin}}/api/saas/v1/reports",
        "description": "Generate a PDF using the nested chart/branding/notes/color payload. Required chart fields: name, dateOfBirth (YYYY-MM-DD), timeOfBirth (24-hour hour/minute), cityId, gender (Male/Female/Other). A successful response is HTTP 201 with only downloadUrl and expiresAt; the script saves them for request 03. Branding appears in the footer; notes appear after report content. Supported themes and options are listed in the collection description. The company must have PDF access enabled and sufficient quota. Each successful send counts as a new report."
      },
      "response": []
    },
    {
      "name": "03 — Download generated PDF",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (!pm.variables.get('downloadUrl')) throw new Error('Generate a report successfully first, or set downloadUrl to its complete private URL.');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('PDF download succeeds', function () { pm.response.to.have.status(200); });",
              "pm.test('Response is a PDF attachment', function () {",
              "  pm.expect(pm.response.headers.get('Content-Type')).to.include('application/pdf');",
              "  pm.expect(pm.response.headers.get('Content-Disposition')).to.include('attachment');",
              "});"
            ]
          }
        }
      ],
      "request": {
        "auth": { "type": "noauth" },
        "method": "GET",
        "header": [{ "key": "Accept", "value": "application/pdf" }],
        "url": "{{downloadUrl}}",
        "description": "Use Send and Download to save the PDF. This uses the exact URL returned by generation, including its private token. No x-api-key or login token is sent. The response is binary application/pdf. HTTP 404 means the link is invalid, expired, revoked, or company download access is disabled. Do not share or log token-bearing links publicly."
      },
      "response": []
    }
  ]
}
