{
  "info": {
    "_postman_id": "4e65e226-e43e-5505-bdf5-6b6ed0714967",
    "name": "Rimo Global Hotels API",
    "description": "Rimo Global Hotels API - test collection.\n\n1. Import this collection and the environment file, and select the environment.\n2. In the environment: paste your token, and put the public IP of the machine running Postman in customerIp (that IP must be whitelisted by Rimo Global).\n3. Collection variables: nationality (a country Name from step 1), cityId, hotelIds (Rimo Global hotel ids from the hotel mapping file), and firstName / lastName before step 7.\n4. Run the steps in order. Each step saves what the next one needs. Results show in the Visualize tab.\n\nEvery booking is live. For certification, book refundable rates several months ahead with real guest names, and cancel each booking right away (steps 9 and 10).\nDates are MM-DD-YYYY. Docs: https://www.rimoglobal.com/apidocs  Support: api@rimoglobal.com",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{token}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "1. Countries",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "(() => {",
              "  const raw = pm.variables.get('token') || '';",
              "  const t = raw.trim().replace(/^bearer\\s+/i, '').replace(/^[\"']|[\"']$/g, '').trim();",
              "  if (!t) throw new Error('token is empty: paste your API token in the environment (variable \"token\")');",
              "  if (t !== raw) pm.environment.set('token', t);",
              "})();",
              "if (!pm.variables.get('customerIp'))",
              "  throw new Error('customerIp is empty: put the public IP of this machine (it must be whitelisted) in the environment');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const list = findArr(norm(pm.response.json())) || [];",
              "pm.test('Got countries', () => pm.expect(list.length).to.be.above(0));",
              "const q = String(V('countrySearch') || '').toLowerCase().trim();",
              "const hits = q ? list.filter(c => String(c.Name).toLowerCase().includes(q)) : list;",
              "if (q && hits[0]) S('countryId', String(hits[0].CountryId));",
              "showTable(q ? `Countries matching \"${q}\" - saved countryId = ${hits[0] ? hits[0].CountryId : 'none'}` : `${list.length} countries (set countrySearch to save one)`,",
              "  ['CountryId', 'Name (use as Nationality)'], hits.slice(0, 300).map(c => [c.CountryId, c.Name]));"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/Countries",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "Countries"
          ]
        },
        "description": "All countries. Set `countrySearch` (for example `Egypt`) to save its CountryId. The country Name is also the value you send as Nationality."
      }
    },
    {
      "name": "2. Cities",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "(() => {",
              "  const raw = pm.variables.get('token') || '';",
              "  const t = raw.trim().replace(/^bearer\\s+/i, '').replace(/^[\"']|[\"']$/g, '').trim();",
              "  if (!t) throw new Error('token is empty: paste your API token in the environment (variable \"token\")');",
              "  if (t !== raw) pm.environment.set('token', t);",
              "})();",
              "if (!pm.variables.get('customerIp'))",
              "  throw new Error('customerIp is empty: put the public IP of this machine (it must be whitelisted) in the environment');",
              "",
              "if (!pm.variables.get('countryId')) throw new Error('Missing countryId: run step 1 with countrySearch set');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const list = findArr(norm(pm.response.json())) || [];",
              "pm.test('Got cities', () => pm.expect(list.length).to.be.above(0));",
              "const q = String(V('citySearch') || '').toLowerCase().trim();",
              "const hits = q ? list.filter(c => String(c.Name).toLowerCase().includes(q)) : list;",
              "if (q && hits[0]) S('cityId', String(hits[0].CityId));",
              "showTable(q ? `Cities matching \"${q}\" - saved cityId = ${hits[0] ? hits[0].CityId : 'none'}` : `${list.length} cities (set citySearch to save one)`,",
              "  ['CityId', 'Name'], hits.slice(0, 300).map(c => [c.CityId, c.Name]));"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"CountryId\": \"{{countryId}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/Cities",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "Cities"
          ]
        },
        "description": "Cities of `countryId`. Set `citySearch` to save a CityId."
      }
    },
    {
      "name": "3. Search - prices only",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "(() => {",
              "  const raw = pm.variables.get('token') || '';",
              "  const t = raw.trim().replace(/^bearer\\s+/i, '').replace(/^[\"']|[\"']$/g, '').trim();",
              "  if (!t) throw new Error('token is empty: paste your API token in the environment (variable \"token\")');",
              "  if (t !== raw) pm.environment.set('token', t);",
              "})();",
              "if (!pm.variables.get('customerIp'))",
              "  throw new Error('customerIp is empty: put the public IP of this machine (it must be whitelisted) in the environment');",
              "",
              "(() => {",
              "  // Default stay: 90 days from today, 1 night (test bookings must be far ahead and refundable)",
              "  const pad = n => String(n).padStart(2, '0');",
              "  const mdy = d => `${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${d.getFullYear()}`;",
              "  if (!pm.variables.get('checkIn')) {",
              "    const a = new Date(); a.setDate(a.getDate() + 90);",
              "    const b = new Date(a); b.setDate(b.getDate() + 1);",
              "    pm.collectionVariables.set('checkIn', mdy(a));",
              "    pm.collectionVariables.set('checkOut', mdy(b));",
              "    pm.collectionVariables.set('nights', '1');",
              "  }",
              "})();",
              "",
              "['cityId', 'nationality', 'hotelIds'].forEach(k => {",
              "  if (!pm.variables.get(k)) throw new Error(`Missing ${k}: set it in the collection variables (hotelIds = comma-separated Rimo Global hotel ids from the hotel mapping file)`);",
              "});"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const j = norm(pm.response.json());",
              "const err = apiErrors(j);",
              "pm.test('No API errors', () => pm.expect(err, JSON.stringify(err)).to.be.null);",
              "const rs = j.AvailabilityRS || {};",
              "const hotels = (rs.HotelResult || []).map(h => ({ id: h.HotelId, price: Number(h.StartPrice ?? h.MinPrice) }))",
              "  .filter(h => h.id && !isNaN(h.price)).sort((a, b) => a.price - b.price);",
              "pm.test('Got prices', () => pm.expect(hotels.length).to.be.above(0));",
              "showTable(`${hotels.length} hotels with a price for ${V('checkIn')} - ${V('checkOut')} (${rs.Currency || ''}). Prices only: run step 4 for rooms.`,",
              "  ['HotelId', 'From'], hotels.map(h => [h.id, h.price]));"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"Rooms\": [\n      { \"RoomNo\": 1, \"NoofAdults\": {{adults}}, \"NoOfChild\": 0, \"ChildAge\": [] }\n    ],\n    \"CityID\": \"{{cityId}}\",\n    \"CheckInDate\": \"{{checkIn}}\",\n    \"CheckOutDate\": \"{{checkOut}}\",\n    \"NoofNights\": \"{{nights}}\",\n    \"Nationality\": \"{{nationality}}\",\n    \"Filters\": {\n      \"IsRecommendedOnly\": \"0\",\n      \"IsShowRooms\": \"0\",\n      \"IsOnlyAvailable\": \"1\",\n      \"StarRating\": { \"Min\": 0, \"Max\": 5 },\n      \"HotelIDs\": \"{{hotelIds}}\"\n    }\n  },\n  \"AdvancedOptions\": {\n    \"Currency\": \"{{currency}}\",\n    \"CustomerIpAddress\": \"{{customerIp}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/Availabilitywithcancellation",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "Availabilitywithcancellation"
          ]
        },
        "description": "Lowest price per hotel for `hotelIds` (IsShowRooms = 0). Fast; use it for listing pages. Send hotel ids: a search without HotelIDs returns no results."
      }
    },
    {
      "name": "4. Search - rooms and cancellation",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "(() => {",
              "  const raw = pm.variables.get('token') || '';",
              "  const t = raw.trim().replace(/^bearer\\s+/i, '').replace(/^[\"']|[\"']$/g, '').trim();",
              "  if (!t) throw new Error('token is empty: paste your API token in the environment (variable \"token\")');",
              "  if (t !== raw) pm.environment.set('token', t);",
              "})();",
              "if (!pm.variables.get('customerIp'))",
              "  throw new Error('customerIp is empty: put the public IP of this machine (it must be whitelisted) in the environment');",
              "",
              "(() => {",
              "  // Default stay: 90 days from today, 1 night (test bookings must be far ahead and refundable)",
              "  const pad = n => String(n).padStart(2, '0');",
              "  const mdy = d => `${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${d.getFullYear()}`;",
              "  if (!pm.variables.get('checkIn')) {",
              "    const a = new Date(); a.setDate(a.getDate() + 90);",
              "    const b = new Date(a); b.setDate(b.getDate() + 1);",
              "    pm.collectionVariables.set('checkIn', mdy(a));",
              "    pm.collectionVariables.set('checkOut', mdy(b));",
              "    pm.collectionVariables.set('nights', '1');",
              "  }",
              "})();",
              "",
              "['cityId', 'nationality', 'hotelIds'].forEach(k => {",
              "  if (!pm.variables.get(k)) throw new Error(`Missing ${k}: set it in the collection variables (hotelIds = comma-separated Rimo Global hotel ids from the hotel mapping file)`);",
              "});"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const j = norm(pm.response.json());",
              "const err = apiErrors(j);",
              "pm.test('No API errors', () => pm.expect(err, JSON.stringify(err)).to.be.null);",
              "const rs = j.AvailabilityRS || {};",
              "S('searchKey', rs.SearchKey || '');",
              "S('searchKeyAt', String(Date.now()));",
              "",
              "const maxP = Number(V('maxPrice') || 50);",
              "const safetyDays = Number(V('minFreeCancelDays') || 7);",
              "const parseD = s => { if (!s) return null; const p = String(s).split(/[-\\/T ]/); return new Date(+p[2], +p[0] - 1, +p[1]); };",
              "const fmt = d => d ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` : '';",
              "const today = new Date(); today.setHours(0, 0, 0, 0);",
              "",
              "const all = [];",
              "(rs.HotelResult || []).forEach(h => (h.HotelOption || []).forEach(o => (o.HotelRooms || []).forEach(combo => {",
              "  // Each inner array of HotelRooms is ONE bookable alternative (one room per searched room, or one combined room)",
              "  const rooms = Array.isArray(combo) ? combo : [combo];",
              "  if (!rooms.length) return;",
              "  const total = Math.round(rooms.reduce((s, r) => s + Number(r.Price || 0), 0) * 100) / 100;",
              "  const available = rooms.every(r => String(r.BookingStatus).toLowerCase() === 'available');",
              "  const policies = rooms.flatMap(r => r.CancellationPolicy || []);",
              "  const firstCharge = policies.filter(p => Number(p.CancellationPrice) > 0).map(p => parseD(p.FromDate)).filter(Boolean).sort((a, b) => a - b)[0] || null;",
              "  // No policy at all = unknown -> treated as NOT refundable",
              "  const freeDays = !policies.length ? -1 : (firstCharge ? Math.floor((firstCharge - today) / 86400000) : 9999);",
              "  all.push({",
              "    hotelId: h.HotelId, optionId: o.HotelOptionId, total, available, firstCharge, freeDays,",
              "    room: rooms.map(r => r.RoomTypeName).join(' + '),",
              "    meal: rooms.map(r => r.MappedMealName || r.MealName).join(' + '),",
              "    rooms: rooms.map(r => ({ RoomNo: String(r.RoomNo), RoomToken: r.RoomToken }))",
              "  });",
              "})));",
              "",
              "const picks = all.filter(x => x.available && x.freeDays >= safetyDays && x.total <= maxP)",
              "  .sort((a, b) => (a.total - b.total) || (b.freeDays - a.freeDays)).slice(0, 50);",
              "const sel = picks[Number(V('pickIndex') || 0)];",
              "pm.test(`Refundable rooms up to ${maxP} found`, () => pm.expect(picks.length).to.be.above(0));",
              "const freeUntil = x => x.firstCharge ? fmt(new Date(x.firstCharge.getTime() - 86400000)) : 'no charge listed';",
              "if (sel) {",
              "  S('hotelOptionId', sel.optionId);",
              "  S('hotelRoomsJson', JSON.stringify(sel.rooms));",
              "  S('selectedSummary', `Hotel ${sel.hotelId} | ${sel.room} | ${sel.total} | free cancellation until ${freeUntil(sel)}`);",
              "} else { S('hotelOptionId', ''); S('hotelRoomsJson', '[]'); S('selectedSummary', ''); }",
              "showTable(`Selected row ${V('pickIndex') || 0}: ${sel ? V('selectedSummary') : 'none'}. Change pickIndex to choose another row. The SearchKey expires in 25 minutes.`,",
              "  ['#', 'HotelId', 'Room', 'Meal', 'Total', 'Free cancellation until', 'Days left'],",
              "  picks.map((x, i) => [i, x.hotelId, x.room, x.meal, x.total, freeUntil(x), x.freeDays === 9999 ? '-' : x.freeDays]));"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"Rooms\": [\n      { \"RoomNo\": 1, \"NoofAdults\": {{adults}}, \"NoOfChild\": 0, \"ChildAge\": [] }\n    ],\n    \"CityID\": \"{{cityId}}\",\n    \"CheckInDate\": \"{{checkIn}}\",\n    \"CheckOutDate\": \"{{checkOut}}\",\n    \"NoofNights\": \"{{nights}}\",\n    \"Nationality\": \"{{nationality}}\",\n    \"Filters\": {\n      \"IsRecommendedOnly\": \"0\",\n      \"IsShowRooms\": \"1\",\n      \"IsOnlyAvailable\": \"1\",\n      \"StarRating\": { \"Min\": 0, \"Max\": 5 },\n      \"HotelIDs\": \"{{hotelIds}}\"\n    }\n  },\n  \"AdvancedOptions\": {\n    \"Currency\": \"{{currency}}\",\n    \"CustomerIpAddress\": \"{{customerIp}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/Availabilitywithcancellation",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "Availabilitywithcancellation"
          ]
        },
        "description": "Rooms, RoomTokens and cancellation policies (IsShowRooms = 1). Saves the SearchKey and selects refundable row `pickIndex` under `maxPrice`. Open the Visualize tab."
      }
    },
    {
      "name": "5. ReCheck",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "(() => {",
              "  const raw = pm.variables.get('token') || '';",
              "  const t = raw.trim().replace(/^bearer\\s+/i, '').replace(/^[\"']|[\"']$/g, '').trim();",
              "  if (!t) throw new Error('token is empty: paste your API token in the environment (variable \"token\")');",
              "  if (t !== raw) pm.environment.set('token', t);",
              "})();",
              "if (!pm.variables.get('customerIp'))",
              "  throw new Error('customerIp is empty: put the public IP of this machine (it must be whitelisted) in the environment');",
              "",
              "if (!pm.variables.get('searchKey')) throw new Error('No searchKey: run step 4 (Search rooms) first');",
              "if (Date.now() - Number(pm.variables.get('searchKeyAt') || 0) > 24 * 60 * 1000)",
              "  throw new Error('searchKey is older than 24 minutes (it expires at 25): run step 4 again');",
              "",
              "if (!pm.variables.get('hotelOptionId')) throw new Error('No room selected: step 4 found no refundable room under maxPrice');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const j = norm(pm.response.json());",
              "const err = apiErrors(j);",
              "pm.test('No API errors', () => pm.expect(err, JSON.stringify(err)).to.be.null);",
              "const rooms = Array.isArray(j.ReCheckRS) ? j.ReCheckRS : (findArr(j.ReCheckRS || {}) || []);",
              "pm.test('All rooms Available', () => pm.expect(rooms.length > 0 && rooms.every(r => String(r.BookingStatus).toLowerCase() === 'available')).to.be.true);",
              "showTable(`ReCheck - ${V('selectedSummary')}. Show the essential information to the guest before payment.`,",
              "  ['Room', 'Meal', 'Price', 'Status', 'Essential information'],",
              "  rooms.map(r => [r.RoomTypeName, r.MappedMealName || r.MealName, r.Price, r.BookingStatus, String(r.EssentialInformation || '').slice(0, 1200)]));"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"SearchKey\": \"{{searchKey}}\",\n    \"HotelOption\": {\n      \"HotelOptionId\": \"{{hotelOptionId}}\",\n      \"HotelRooms\": {{hotelRoomsJson}}\n    }\n  },\n  \"AdvancedOptions\": {\n    \"Currency\": \"{{currency}}\",\n    \"CustomerIpAddress\": \"{{customerIp}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/ReCheck",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "ReCheck"
          ]
        },
        "description": "Live price, availability, cancellation policy and essential information for the selected rooms. Call once per booking."
      }
    },
    {
      "name": "6. PreBook",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "(() => {",
              "  const raw = pm.variables.get('token') || '';",
              "  const t = raw.trim().replace(/^bearer\\s+/i, '').replace(/^[\"']|[\"']$/g, '').trim();",
              "  if (!t) throw new Error('token is empty: paste your API token in the environment (variable \"token\")');",
              "  if (t !== raw) pm.environment.set('token', t);",
              "})();",
              "if (!pm.variables.get('customerIp'))",
              "  throw new Error('customerIp is empty: put the public IP of this machine (it must be whitelisted) in the environment');",
              "",
              "if (!pm.variables.get('searchKey')) throw new Error('No searchKey: run step 4 (Search rooms) first');",
              "if (Date.now() - Number(pm.variables.get('searchKeyAt') || 0) > 24 * 60 * 1000)",
              "  throw new Error('searchKey is older than 24 minutes (it expires at 25): run step 4 again');",
              "",
              "if (!pm.variables.get('hotelOptionId')) throw new Error('No room selected: step 4 found no refundable room under maxPrice');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const j = norm(pm.response.json());",
              "const err = apiErrors(j);",
              "pm.test('No API errors', () => pm.expect(err, JSON.stringify(err)).to.be.null);",
              "const o = (j.PreBookRS || {}).HotelOption || {};",
              "pm.test('Status is Bookable', () => pm.expect(o.Status).to.eql('Bookable'));",
              "pm.test('No price change (IsPriceChange = 0)', () => pm.expect(Number(o.IsPriceChange || 0)).to.eql(0));",
              "S('bookingToken', o.BookingToken || '');",
              "S('prebookTotal', String(o.TotalPrice ?? ''));",
              "S('prebookRoomsJson', JSON.stringify((o.HotelRooms || []).map(r => ({ UniqueId: r.UniqueId, RoomNo: String(r.RoomNo) }))));",
              "showTable(`PreBook: ${o.HotelName || ''} | ${o.CheckInDate} to ${o.CheckOutDate} | Total ${o.TotalPrice} | ${o.Status}`,",
              "  ['UniqueId', 'RoomNo', 'Room', 'Meal', 'Price'],",
              "  (o.HotelRooms || []).map(r => [r.UniqueId, r.RoomNo, r.RoomTypeName, r.MappedMealName || r.MealName, r.Price]));"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"SearchKey\": \"{{searchKey}}\",\n    \"HotelOption\": {\n      \"HotelOptionId\": \"{{hotelOptionId}}\",\n      \"HotelRooms\": {{hotelRoomsJson}}\n    }\n  },\n  \"AdvancedOptions\": {\n    \"Currency\": \"{{currency}}\",\n    \"CustomerIpAddress\": \"{{customerIp}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/PreBook",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "PreBook"
          ]
        },
        "description": "Returns the BookingToken and one UniqueId per room. Book only when Status is Bookable. Call once per booking."
      }
    },
    {
      "name": "7. Book",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "(() => {",
              "  const raw = pm.variables.get('token') || '';",
              "  const t = raw.trim().replace(/^bearer\\s+/i, '').replace(/^[\"']|[\"']$/g, '').trim();",
              "  if (!t) throw new Error('token is empty: paste your API token in the environment (variable \"token\")');",
              "  if (t !== raw) pm.environment.set('token', t);",
              "})();",
              "if (!pm.variables.get('customerIp'))",
              "  throw new Error('customerIp is empty: put the public IP of this machine (it must be whitelisted) in the environment');",
              "",
              "if (!pm.variables.get('searchKey')) throw new Error('No searchKey: run step 4 (Search rooms) first');",
              "if (Date.now() - Number(pm.variables.get('searchKeyAt') || 0) > 24 * 60 * 1000)",
              "  throw new Error('searchKey is older than 24 minutes (it expires at 25): run step 4 again');",
              "",
              "const V = k => pm.variables.get(k);",
              "const fn = (V('firstName') || '').trim();",
              "const ln = (V('lastName') || '').trim();",
              "if (!fn || !ln) throw new Error('Set firstName and lastName (the real guest name, Latin letters) before booking');",
              "if (!/^[A-Za-z][A-Za-z '\\-]*$/.test(fn + ln)) throw new Error('Names must use Latin letters only');",
              "if (!V('bookingToken')) throw new Error('No bookingToken: run step 6 (PreBook) first');",
              "const maxP = Number(V('maxPrice') || 50);",
              "const tot = Number(V('prebookTotal'));",
              "if (!(tot <= maxP)) throw new Error(`PreBook total ${tot} is above maxPrice ${maxP}: not booking`);",
              "const adults = Math.max(1, Number(V('adults') || 1));",
              "if (adults > 2) throw new Error('This collection books 1 or 2 adults per room. For more guests, add one entry per guest to bookRoomsJson yourself');",
              "if (adults === 2 && (!(V('firstName2') || '').trim() || !(V('lastName2') || '').trim())) throw new Error('2 adults: set firstName2 and lastName2 (every guest needs a real name)');",
              "const rooms = JSON.parse(V('prebookRoomsJson') || '[]');",
              "if (!rooms.length) throw new Error('No rooms from PreBook');",
              "// One entry per guest. UniqueId and RoomNo come from PreBook. IsLead = 1 on the first guest only.",
              "const pax = [];",
              "rooms.forEach(r => {",
              "  for (let i = 0; i < adults; i++) {",
              "    const second = i > 0;",
              "    pax.push({",
              "      UniqueId: r.UniqueId, RoomNo: String(r.RoomNo), IsLead: pax.length === 0 ? '1' : '0', PaxType: 'Adult',",
              "      Prefix: ((second ? V('title2') : V('title')) || 'Mr').replace(/\\.?$/, '.'),",
              "      FirstName: second ? V('firstName2').trim() : fn,",
              "      LastName: second ? V('lastName2').trim() : ln,",
              "      ChildAge: '0'",
              "    });",
              "  }",
              "});",
              "pm.collectionVariables.set('bookRoomsJson', JSON.stringify(pax));",
              "pm.collectionVariables.set('internalRef', 'TEST-' + Date.now());"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const j = norm(pm.response.json());",
              "const err = apiErrors(j);",
              "pm.test('No API errors', () => pm.expect(err, JSON.stringify(err)).to.be.null);",
              "const b = j.BookRS || {};",
              "if (b.ReferenceNo) S('referenceNo', b.ReferenceNo);",
              "if (b.BookingId) S('bookingId', String(b.BookingId));",
              "pm.test('Got ReferenceNo', () => pm.expect(b.ReferenceNo).to.be.ok);",
              "showTable(`Book sent: ${b.ReferenceNo || 'no reference'}. Now run step 8 (BookingDetail): only BookingDetail tells the real status.`,",
              "  ['Field', 'Value'],",
              "  [['BookingId (0 is normal here)', b.BookingId], ['ReferenceNo', b.ReferenceNo], ['InternalReference', b.InternalReference || V('internalRef')], ['Status', b.Status]]);"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"SearchKey\": \"{{searchKey}}\",\n    \"BookRQ\": {\n      \"BookingToken\": \"{{bookingToken}}\",\n      \"TotalPrice\": {{prebookTotal}},\n      \"InternalReference\": \"{{internalRef}}\",\n      \"HotelRooms\": {{bookRoomsJson}}\n    }\n  },\n  \"AdvancedOptions\": {\n    \"Currency\": \"{{currency}}\",\n    \"CustomerIpAddress\": \"{{customerIp}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/Book",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "Book"
          ]
        },
        "description": "Creates a REAL booking. Needs firstName / lastName (real guest, Latin letters). Refuses to send above maxPrice. Never send Book twice: after any error or timeout, run BookingDetail."
      }
    },
    {
      "name": "8. BookingDetail",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (!pm.variables.get('referenceNo') && !pm.variables.get('internalRef')) throw new Error('No referenceNo / internalRef yet: run step 7 (Book) first');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const j = norm(pm.response.json());",
              "const err = apiErrors(j);",
              "pm.test('No API errors', () => pm.expect(err, JSON.stringify(err)).to.be.null);",
              "const d = (j.BookingDetailRS || {}).HotelOption || {};",
              "if (d.BookingId) S('bookingId', String(d.BookingId));",
              "if (d.ReferenceNo) S('referenceNo', d.ReferenceNo);",
              "const st = (d.HotelRooms || []).map(r => r.BookingStatus);",
              "pm.test('Every room Confirmed', () => pm.expect(st.length > 0 && st.every(s => /^confirmed$/i.test(String(s)))).to.be.true);",
              "showTable(`Booking ${d.ReferenceNo || ''} - room status: ${st.join(', ') || 'none'}`, ['Field', 'Value'],",
              "  Object.entries(d).filter(([k]) => /^[A-Z]/.test(k)).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v).slice(0, 1200) : v]));"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"BookingDetailRQ\": {\n      \"BookingId\": \"0\",\n      \"ReferenceNo\": \"{{referenceNo}}\",\n      \"InternalReference\": \"{{internalRef}}\"\n    }\n  },\n  \"AdvancedOptions\": {\n    \"Currency\": \"{{currency}}\",\n    \"CustomerIpAddress\": \"{{customerIp}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/BookingDetail",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "BookingDetail"
          ]
        },
        "description": "Mandatory after every Book. The booking is sold only when every room is Confirmed."
      }
    },
    {
      "name": "9. Cancellation charges",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (!pm.variables.get('referenceNo')) throw new Error('No referenceNo: run steps 7 and 8 first');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const j = norm(pm.response.json());",
              "const err = apiErrors(j);",
              "pm.test('No API errors', () => pm.expect(err, JSON.stringify(err)).to.be.null);",
              "const o = (j.CheckHotelCancellationChargesRS || {}).HotelOption || {};",
              "if (o.CancelCode) S('cancelCode', o.CancelCode);",
              "const rooms = o.HotelRooms || [];",
              "if (rooms[0] && rooms[0].BookingDetailId) S('bookingDetailId', String(rooms[0].BookingDetailId));",
              "pm.test('Got CancelCode', () => pm.expect(o.CancelCode).to.be.ok);",
              "pm.test('No cancellation charge', () => pm.expect(Number(o.TotalCharge || 0)).to.eql(0));",
              "showTable(`Cancellation charge ${o.TotalCharge} / refund ${o.TotalRefund}`, ['Field', 'Value'],",
              "  [['BookingId', o.BookingId], ['TotalPrice', o.TotalPrice], ['TotalCharge', o.TotalCharge], ['TotalRefund', o.TotalRefund],",
              "   ['BookingDetailId', rooms.map(r => r.BookingDetailId).join(', ')]]);"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"CheckHotelCancellationChargesRQ\": {\n      \"BookingId\": \"{{bookingId}}\",\n      \"InternalReference\": \"{{internalRef}}\",\n      \"ReferenceNo\": \"{{referenceNo}}\"\n    }\n  },\n  \"AdvancedOptions\": {\n    \"Currency\": \"{{currency}}\",\n    \"CustomerIpAddress\": \"{{customerIp}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/CheckHotelCancellationCharges",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "CheckHotelCancellationCharges"
          ]
        },
        "description": "What cancelling costs right now. Saves CancelCode and BookingDetailId for step 10."
      }
    },
    {
      "name": "10. Cancel booking",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "['bookingId', 'bookingDetailId', 'cancelCode'].forEach(k => { if (!pm.variables.get(k)) throw new Error(`Missing ${k}: run step 9 first`); });"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const j = norm(pm.response.json());",
              "const err = apiErrors(j);",
              "pm.test('No API errors', () => pm.expect(err, JSON.stringify(err)).to.be.null);",
              "const c = (j.CancelRS || {}).HotelOption || {};",
              "const rooms = c.HotelRooms || [];",
              "pm.test('Every room cancelled (CancelStatus = 1)', () => pm.expect(rooms.length > 0 && rooms.every(r => Number(r.CancelStatus) === 1)).to.be.true);",
              "showTable(`Cancel booking ${c.BookingId || V('bookingId')}. Run step 8 again: BookingDetail must show Cancelled.`, ['Room', 'Message', 'CancelStatus'],",
              "  rooms.map(r => [r.RoomTypeName, r.Message, r.CancelStatus]));"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"CancelRQ\": {\n      \"BookingId\": {{bookingId}},\n      \"BookingDetailId\": {{bookingDetailId}},\n      \"CancelCode\": \"{{cancelCode}}\",\n      \"CancelAll\": 1,\n      \"Reason\": \"API test booking\"\n    }\n  },\n  \"AdvancedOptions\": {\n    \"Currency\": \"{{currency}}\",\n    \"CustomerIpAddress\": \"{{customerIp}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/CancelBooking",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "CancelBooking"
          ]
        },
        "description": "Cancels the whole booking (CancelAll = 1). Run step 8 afterwards: it must show Cancelled."
      }
    },
    {
      "name": "11. Booking search",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "(() => {",
              "  const raw = pm.variables.get('token') || '';",
              "  const t = raw.trim().replace(/^bearer\\s+/i, '').replace(/^[\"']|[\"']$/g, '').trim();",
              "  if (!t) throw new Error('token is empty: paste your API token in the environment (variable \"token\")');",
              "  if (t !== raw) pm.environment.set('token', t);",
              "})();",
              "if (!pm.variables.get('customerIp'))",
              "  throw new Error('customerIp is empty: put the public IP of this machine (it must be whitelisted) in the environment');",
              "",
              "(() => {",
              "  const pad = n => String(n).padStart(2, '0');",
              "  const mdy = d => `${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${d.getFullYear()}`;",
              "  if (!pm.variables.get('fromDate')) { const a = new Date(); a.setDate(a.getDate() - 7); pm.collectionVariables.set('fromDate', mdy(a)); }",
              "  if (!pm.variables.get('toDate')) pm.collectionVariables.set('toDate', mdy(new Date()));",
              "})();"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "function showTable(title, cols, rows) {",
              "  const tpl = `<h3 style=\"font-family:sans-serif\">{{title}}</h3>",
              "  <table style=\"border-collapse:collapse;font-family:sans-serif;font-size:13px\">",
              "  <tr>{{#each cols}}<th style=\"border:1px solid #ccc;padding:6px;background:#f3f3f3;text-align:left\">{{this}}</th>{{/each}}</tr>",
              "  {{#each rows}}<tr>{{#each this}}<td style=\"border:1px solid #ccc;padding:6px\">{{this}}</td>{{/each}}</tr>{{/each}}",
              "  </table>`;",
              "  pm.visualizer.set(tpl, { title, cols, rows });",
              "}",
              "function findArr(o) {",
              "  if (Array.isArray(o)) return o;",
              "  for (const v of Object.values(o || {})) {",
              "    if (v && typeof v === 'object') { const r = findArr(v); if (r) return r; }",
              "  }",
              "  return null;",
              "}",
              "function apiErrors(j) {",
              "  const e = j && (j.Error || j.error);",
              "  return Array.isArray(e) && e.length ? e : null;",
              "}",
              "const V = k => pm.variables.get(k);",
              "const S = (k, v) => pm.collectionVariables.set(k, v);",
              "// Responses can come back in camelCase (bookingToken, bookRS...). Add PascalCase aliases so both work.",
              "function norm(o) {",
              "  if (Array.isArray(o)) return o.map(norm);",
              "  if (!o || typeof o !== 'object') return o;",
              "  const out = {};",
              "  for (const [k, v] of Object.entries(o)) {",
              "    const nv = norm(v);",
              "    out[k] = nv;",
              "    const P = k.charAt(0).toUpperCase() + k.slice(1);",
              "    if (!(P in o)) out[P] = nv;",
              "  }",
              "  return out;",
              "}",
              "",
              "const j = norm(pm.response.json());",
              "const err = apiErrors(j);",
              "pm.test('No API errors', () => pm.expect(err, JSON.stringify(err)).to.be.null);",
              "const list = ((j.BookingSearchRS || {}).Bookings) || findArr(j.BookingSearchRS || {}) || [];",
              "showTable(`${list.length} bookings`, ['ReferenceNo', 'InternalReference', 'Hotel', 'Check-in', 'Check-out', 'Total', 'Status'],",
              "  list.map(b => [b.ReferenceNo, b.InternalReference, b.HotelName, b.CheckInDate, b.CheckOutDate, b.TotalPrice, b.Status]));"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"Request\": {\n    \"BookingSearchRQ\": {\n      \"SearchBy\": \"bookingdate\",\n      \"SearchCriteria\": {\n        \"Dates\": { \"FromDate\": \"{{fromDate}}\", \"ToDate\": \"{{toDate}}\" }\n      }\n    }\n  },\n  \"AdvancedOptions\": {\n    \"Currency\": \"{{currency}}\",\n    \"CustomerIpAddress\": \"{{customerIp}}\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/xconnect/BookingSearch",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "xconnect",
            "BookingSearch"
          ]
        },
        "description": "Your bookings made between fromDate and toDate (MM-DD-YYYY, default: the last 7 days)."
      }
    }
  ],
  "variable": [
    {
      "key": "countrySearch",
      "value": "",
      "type": "string"
    },
    {
      "key": "countryId",
      "value": "",
      "type": "string"
    },
    {
      "key": "citySearch",
      "value": "",
      "type": "string"
    },
    {
      "key": "cityId",
      "value": "",
      "type": "string"
    },
    {
      "key": "nationality",
      "value": "",
      "type": "string"
    },
    {
      "key": "hotelIds",
      "value": "",
      "type": "string"
    },
    {
      "key": "checkIn",
      "value": "",
      "type": "string"
    },
    {
      "key": "checkOut",
      "value": "",
      "type": "string"
    },
    {
      "key": "nights",
      "value": "1",
      "type": "string"
    },
    {
      "key": "adults",
      "value": "1",
      "type": "string"
    },
    {
      "key": "maxPrice",
      "value": "50",
      "type": "string"
    },
    {
      "key": "minFreeCancelDays",
      "value": "7",
      "type": "string"
    },
    {
      "key": "pickIndex",
      "value": "0",
      "type": "string"
    },
    {
      "key": "title",
      "value": "Mr",
      "type": "string"
    },
    {
      "key": "firstName",
      "value": "",
      "type": "string"
    },
    {
      "key": "lastName",
      "value": "",
      "type": "string"
    },
    {
      "key": "title2",
      "value": "Mrs",
      "type": "string"
    },
    {
      "key": "firstName2",
      "value": "",
      "type": "string"
    },
    {
      "key": "lastName2",
      "value": "",
      "type": "string"
    },
    {
      "key": "fromDate",
      "value": "",
      "type": "string"
    },
    {
      "key": "toDate",
      "value": "",
      "type": "string"
    },
    {
      "key": "searchKey",
      "value": "",
      "type": "string"
    },
    {
      "key": "searchKeyAt",
      "value": "",
      "type": "string"
    },
    {
      "key": "hotelOptionId",
      "value": "",
      "type": "string"
    },
    {
      "key": "hotelRoomsJson",
      "value": "[]",
      "type": "string"
    },
    {
      "key": "selectedSummary",
      "value": "",
      "type": "string"
    },
    {
      "key": "bookingToken",
      "value": "",
      "type": "string"
    },
    {
      "key": "prebookTotal",
      "value": "",
      "type": "string"
    },
    {
      "key": "prebookRoomsJson",
      "value": "[]",
      "type": "string"
    },
    {
      "key": "bookRoomsJson",
      "value": "[]",
      "type": "string"
    },
    {
      "key": "internalRef",
      "value": "",
      "type": "string"
    },
    {
      "key": "referenceNo",
      "value": "",
      "type": "string"
    },
    {
      "key": "bookingId",
      "value": "0",
      "type": "string"
    },
    {
      "key": "bookingDetailId",
      "value": "",
      "type": "string"
    },
    {
      "key": "cancelCode",
      "value": "",
      "type": "string"
    }
  ]
}