{
  "info": {
    "name": "OlympusPay — Travel (Duffel)",
    "_postman_id": "olympuspay-travel-v1",
    "description": "Travel module API tests for OlympusPay using the Duffel travel API.\n\nAll requests require {{USER_ACCESS_TOKEN}} to be set.\nTo get a token: run Auth → Login first.\n\nNEVER expose DUFFEL_API_KEY in this collection — it lives in Supabase Vault only.\nThe edge functions handle all Duffel authentication server-side.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [{ "key": "token", "value": "{{USER_ACCESS_TOKEN}}", "type": "string" }]
  },
  "variable": [
    { "key": "base_url", "value": "{{FUNCTIONS_BASE_URL}}" }
  ],
  "item": [
    {
      "name": "Flights",
      "item": [
        {
          "name": "1. Search flights (one-way)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "const r = pm.response.json();",
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Has offers', () => pm.expect(r.offers).to.be.an('array'));",
                  "pm.test('Has offer_request_id', () => pm.expect(r.offer_request_id).to.be.a('string'));",
                  "if (r.offer_request_id) pm.environment.set('DUFFEL_OFFER_REQUEST_ID', r.offer_request_id);",
                  "if (r.offers?.[0]?.id) pm.environment.set('DUFFEL_OFFER_ID', r.offers[0].id);",
                  "pm.test('Each offer has total_amount', () => r.offers?.forEach(o => pm.expect(o.total_amount).to.be.a('string')));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": "{{base_url}}/duffel-search",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"trip_type\": \"one_way\",\n  \"slices\": [\n    {\n      \"origin\": \"{{TEST_ORIGIN}}\",\n      \"destination\": \"{{TEST_DESTINATION}}\",\n      \"departure_date\": \"{{TEST_DEPART_DATE}}\"\n    }\n  ],\n  \"passengers\": [{ \"type\": \"adult\" }],\n  \"cabin_class\": \"economy\",\n  \"direct_only\": false\n}"
            }
          }
        },
        {
          "name": "2. Search flights (return)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": "{{base_url}}/duffel-search",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"trip_type\": \"return\",\n  \"slices\": [\n    { \"origin\": \"{{TEST_ORIGIN}}\", \"destination\": \"{{TEST_DESTINATION}}\", \"departure_date\": \"{{TEST_DEPART_DATE}}\" },\n    { \"origin\": \"{{TEST_DESTINATION}}\", \"destination\": \"{{TEST_ORIGIN}}\", \"departure_date\": \"2026-12-08\" }\n  ],\n  \"passengers\": [{ \"type\": \"adult\" }],\n  \"cabin_class\": \"economy\"\n}"
            }
          }
        },
        {
          "name": "3. Re-fetch offers (sort by duration)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "pm.test('Has offers', () => pm.expect(pm.response.json().offers).to.be.an('array'));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/duffel-search?offer_request_id={{DUFFEL_OFFER_REQUEST_ID}}&sort=total_amount",
              "query": [
                { "key": "offer_request_id", "value": "{{DUFFEL_OFFER_REQUEST_ID}}" },
                { "key": "sort", "value": "total_amount" }
              ]
            }
          }
        },
        {
          "name": "4. Get offer detail + seat maps",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "const r = pm.response.json();",
                  "pm.test('Has offer', () => pm.expect(r.offer).to.be.an('object'));",
                  "pm.test('Offer has slices', () => pm.expect(r.offer?.slices).to.be.an('array').with.length.greaterThan(0));",
                  "pm.test('Seat maps returned', () => pm.expect(r.seat_maps).to.be.an('array'));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/duffel-offer?offer_id={{DUFFEL_OFFER_ID}}",
              "query": [{ "key": "offer_id", "value": "{{DUFFEL_OFFER_ID}}" }]
            }
          }
        },
        {
          "name": "5. Book flight",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Status 201', () => pm.response.to.have.status(201));",
                  "const r = pm.response.json();",
                  "pm.test('Has booking_reference', () => pm.expect(r.booking_reference).to.be.a('string'));",
                  "pm.test('Has order', () => pm.expect(r.order).to.be.an('object'));",
                  "if (r.booking_id) pm.environment.set('DUFFEL_BOOKING_ID', r.booking_id);",
                  "if (r.order?.id)  pm.environment.set('DUFFEL_ORDER_ID', r.order.id);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": "{{base_url}}/duffel-book",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"offer_id\": \"{{DUFFEL_OFFER_ID}}\",\n  \"passengers\": [\n    {\n      \"id\": \"REPLACE_WITH_PASSENGER_ID_FROM_OFFER\",\n      \"title\": \"mr\",\n      \"given_name\": \"Test\",\n      \"family_name\": \"Traveller\",\n      \"gender\": \"m\",\n      \"born_on\": \"1990-01-15\",\n      \"email\": \"test@olympuspay.co\",\n      \"phone_number\": \"+26771000000\"\n    }\n  ],\n  \"selected_services\": []\n}"
            }
          }
        },
        {
          "name": "6. List my bookings",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Status 200', () => pm.response.to.have.status(200));",
                  "const r = pm.response.json();",
                  "pm.test('Has upcoming array', () => pm.expect(r.upcoming).to.be.an('array'));",
                  "pm.test('Has past array', () => pm.expect(r.past).to.be.an('array'));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "url": "{{base_url}}/duffel-orders"
          }
        },
        {
          "name": "7. Get single booking",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Status 200 or 404', () => pm.expect([200, 404]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) {",
                  "  pm.test('Has booking', () => pm.expect(pm.response.json().booking).to.be.an('object'));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/duffel-orders?id={{DUFFEL_BOOKING_ID}}",
              "query": [{ "key": "id", "value": "{{DUFFEL_BOOKING_ID}}" }]
            }
          }
        },
        {
          "name": "8. Cancel booking",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "const r = pm.response.json();",
                  "pm.test('Response received', () => pm.expect([200, 422, 500]).to.include(pm.response.code));",
                  "if (pm.response.code === 200) pm.test('cancelled = true', () => pm.expect(r.cancelled).to.be.true);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": "{{base_url}}/duffel-cancel",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"booking_id\": \"{{DUFFEL_BOOKING_ID}}\"\n}"
            }
          }
        }
      ]
    },
    {
      "name": "Hotels (Stays)",
      "item": [
        {
          "name": "1. Start hotel search",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Status 201', () => pm.response.to.have.status(201));",
                  "const r = pm.response.json();",
                  "pm.test('Has search_id', () => pm.expect(r.search_id).to.be.a('string'));",
                  "if (r.search_id) pm.environment.set('DUFFEL_SEARCH_ID', r.search_id);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": "{{base_url}}/duffel-stays-search",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"check_in_date\": \"2026-12-01\",\n  \"check_out_date\": \"2026-12-04\",\n  \"rooms\": 1,\n  \"guests\": [{ \"type\": \"adult\" }],\n  \"location\": { \"iata_code\": \"JNB\" }\n}"
            }
          }
        },
        {
          "name": "2. Poll hotel results",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/duffel-stays-search?search_id={{DUFFEL_SEARCH_ID}}",
              "query": [{ "key": "search_id", "value": "{{DUFFEL_SEARCH_ID}}" }]
            }
          }
        }
      ]
    },
    {
      "name": "Error handling",
      "item": [
        {
          "name": "Search — invalid airport code",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Should return 4xx', () => pm.expect(pm.response.code).to.be.within(400, 499));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": "{{base_url}}/duffel-search",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"trip_type\": \"one_way\",\n  \"slices\": [{ \"origin\": \"ZZZ\", \"destination\": \"ZZZ\", \"departure_date\": \"2026-12-01\" }],\n  \"passengers\": [{ \"type\": \"adult\" }]\n}"
            }
          }
        },
        {
          "name": "Get offer — non-existent ID",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Should return 404', () => pm.response.to.have.status(404));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/duffel-offer?offer_id=off_nonexistent",
              "query": [{ "key": "offer_id", "value": "off_nonexistent" }]
            }
          }
        },
        {
          "name": "Book — expired offer",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Should return 422 or 500', () => pm.expect([422, 500]).to.include(pm.response.code));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": "{{base_url}}/duffel-book",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"offer_id\": \"off_expired_test_id\",\n  \"passengers\": [{ \"id\": \"pas_test\", \"given_name\": \"Test\", \"family_name\": \"User\" }]\n}"
            }
          }
        }
      ]
    }
  ]
}
