{
  "info": {
    "_postman_id": "04b5469c-5a5c-43ca-98d3-56f792413186",
    "name": "Safiri Third-Party Agent API",
    "description": "Third-party agent account, flight booking, generic ticket, and profile requests.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{jwt_token}}",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "const dateOnly = (date) => date.toISOString().slice(0, 10);",
          "const today = new Date();",
          "const currentDeparture = pm.environment.get('departure_date');",
          "const parsedDeparture = new Date(`${currentDeparture}T00:00:00Z`);",
          "const departureIsInvalid = !currentDeparture || Number.isNaN(parsedDeparture.getTime()) || parsedDeparture <= today;",
          "let departure = parsedDeparture;",
          "",
          "if (departureIsInvalid) {",
          "  departure = new Date(today);",
          "  departure.setUTCDate(departure.getUTCDate() + 30);",
          "  pm.environment.set('departure_date', dateOnly(departure));",
          "}",
          "",
          "const currentReturn = pm.environment.get('return_date');",
          "const parsedReturn = new Date(`${currentReturn}T00:00:00Z`);",
          "if (!currentReturn || Number.isNaN(parsedReturn.getTime()) || parsedReturn <= departure) {",
          "  const returnDate = new Date(departure);",
          "  returnDate.setUTCDate(returnDate.getUTCDate() + 7);",
          "  pm.environment.set('return_date', dateOnly(returnDate));",
          "}"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "Agent account",
      "item": [
        {
          "name": "Register",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{agent_email}}\",\n  \"password\": \"{{agent_password}}\",\n  \"fullName\": \"Agent User\",\n  \"phoneNumber\": \"712345678\",\n  \"countryCode\": \"255\",\n  \"countryCodeName\": \"Tanzania\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/register",
              "host": ["{{base_url}}"],
              "path": ["api", "register"]
            },
            "description": "Register the account used for the third-party agent integration."
          }
        },
        {
          "name": "Log in",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Login succeeds', () => pm.response.to.be.success);",
                  "const body = pm.response.json();",
                  "const token = body.token || body.jwt || body.data?.token;",
                  "pm.expect(token, 'Bearer token').to.be.ok;",
                  "pm.environment.set('jwt_token', token);"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{agent_email}}\",\n  \"password\": \"{{agent_password}}\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/logon",
              "host": ["{{base_url}}"],
              "path": ["api", "logon"]
            }
          }
        },
        {
          "name": "Get profile",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/user",
              "host": ["{{base_url}}"],
              "path": ["api", "user"]
            }
          }
        }
      ]
    },
    {
      "name": "Flight booking",
      "description": "Run in order. Never use the changed-price request until the traveler approves the latest price.",
      "item": [
        {
          "name": "1. Get airport stops",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Airport stops are returned', () => pm.response.to.be.success);",
                  "const stops = pm.response.json();",
                  "pm.expect(stops).to.be.an('array').that.is.not.empty;",
                  "pm.expect(stops[0].stop_code, 'stop_code').to.match(/^[A-Z0-9]{3}$/);"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{gtfs_flight_url}}/api/stops",
              "host": ["{{gtfs_flight_url}}"],
              "path": ["api", "stops"]
            },
            "description": "Use stop_code directly as originLocationCode or destinationLocationCode. If needed, you can also extract the three-letter prefix from stop_id. Do not send the complete stop_id."
          }
        },
        {
          "name": "2. Search flights",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Search succeeds', () => pm.response.to.be.success);",
                  "const body = pm.response.json();",
                  "const offers = Array.isArray(body.data) ? body.data : [];",
                  "const selected = offers[0];",
                  "pm.environment.unset('offer_reference');",
                  "pm.environment.unset('suggested_departure_date');",
                  "pm.environment.unset('suggested_return_date');",
                  "pm.environment.unset('price_version');",
                  "pm.environment.unset('price_amount');",
                  "pm.environment.unset('price_currency');",
                  "pm.test('Count describes requested-date offers', () => {",
                  "  pm.expect(body.meta?.count).to.equal(offers.length);",
                  "});",
                  "",
                  "if (selected) {",
                  "  pm.test('Requested-date offer can be reviewed', () => {",
                  "    pm.expect(selected.offerReference, 'offerReference').to.be.ok;",
                  "    pm.expect(body.meta?.availability?.status).to.equal('RESULTS_FOUND');",
                  "    pm.expect(body.meta?.availability?.lookAhead?.status).to.equal('NOT_NEEDED');",
                  "  });",
                  "  pm.environment.set('offer_reference', selected.offerReference);",
                  "} else {",
                  "  const availability = body.meta?.availability;",
                  "  pm.test('Empty requested-date result has an actionable look-ahead state', () => {",
                  "    pm.expect(body.meta?.count).to.equal(0);",
                  "    pm.expect(availability?.status).to.equal('NO_RESULTS');",
                  "    pm.expect(['FOUND', 'NO_RESULTS', 'INCOMPLETE']).to.include(availability?.lookAhead?.status);",
                  "  });",
                  "",
                  "  if (availability?.lookAhead?.status === 'FOUND') {",
                  "    pm.environment.set('suggested_departure_date', availability.next.departureDate);",
                  "    if (availability.next.returnDate) {",
                  "      pm.environment.set('suggested_return_date', availability.next.returnDate);",
                  "    }",
                  "    console.warn('Confirm the suggested dates, copy them into departure_date/return_date, then rerun the exact search. No offerReference was created.');",
                  "  } else {",
                  "    console.warn('No offerReference was created because the requested-date search was empty.');",
                  "  }",
                  "",
                  "  pm.execution.setNextRequest(null);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Content-Language",
                "value": "en"
              },
              {
                "key": "Content-Currency",
                "value": "USD"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"originLocationCode\": \"{{origin_airport}}\",\n  \"destinationLocationCode\": \"{{destination_airport}}\",\n  \"departureDate\": \"{{departure_date}}\",\n  \"returnDate\": \"{{return_date}}\",\n  \"adults\": 1,\n  \"children\": 0,\n  \"infants\": 0,\n  \"travelClass\": \"ECONOMY\",\n  \"nonStop\": false,\n  \"currencyCode\": \"USD\",\n  \"max\": 10,\n  \"lookAheadDays\": 7\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/flight/search",
              "host": ["{{base_url}}"],
              "path": ["api", "flight", "search"]
            },
            "description": "This example opts in to checking exactly seven later date pairs after an empty requested-date result. POST /api/flight/offers/search is an identical alias. If data contains offers, the first offerReference is saved for this example. If data is empty, no offerReference is created and the collection run stops. For FOUND, inspect suggested_departure_date and suggested_return_date, obtain confirmation, copy them into departure_date and return_date, and rerun this exact request; all other filters and lookAheadDays stay unchanged."
          }
        },
        {
          "name": "3. Review selected offer",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"offerReference\": \"{{offer_reference}}\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/flight/offer",
              "host": ["{{base_url}}"],
              "path": ["api", "flight", "offer"]
            }
          }
        },
        {
          "name": "4. Create reservation",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "if (pm.response.code >= 200 && pm.response.code < 300 && pm.response.code !== 202) {",
                  "  const reservation = body.reservation || {};",
                  "  const ticketId = reservation.ticketIds?.[0] || body.tickets?.[0]?.ticketId;",
                  "  pm.environment.set('reservation_id', reservation.reservationId);",
                  "  pm.environment.set('booking_reference', reservation.bookingReference);",
                  "  pm.environment.set('ticket_id', ticketId);",
                  "}",
                  "if (body.errorCode === 'FLIGHT_PRICE_CHANGED') {",
                  "  pm.environment.set('price_version', body.data.priceVersion);",
                  "  pm.environment.set('price_amount', body.data.currentPrice.grandTotal);",
                  "  pm.environment.set('price_currency', body.data.currentPrice.currency);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-External-User-Id",
                "value": "{{external_user_id}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"offerReference\": \"{{offer_reference}}\",\n  \"passengers\": [\n    {\n      \"firstName\": \"Asha\",\n      \"lastName\": \"Mushi\",\n      \"email\": \"asha.mushi@example.com\",\n      \"countryCode\": \"255\",\n      \"phoneNumber\": \"712345678\",\n      \"dateOfBirth\": \"1990-05-12\",\n      \"gender\": \"FEMALE\",\n      \"ageCategory\": \"ADULT\",\n      \"documentType\": \"PASSPORT\",\n      \"passportNumber\": \"TZ1234567\",\n      \"passportExpiryDate\": \"2030-05-12\",\n      \"documentIssuingCountry\": \"TZ\",\n      \"nationality\": \"TZ\"\n    }\n  ],\n  \"selectedOption\": \"NON_REFUNDABLE_FARE\",\n  \"notifications\": {\n    \"safiriAccountCreation\": true,\n    \"ticketPurchase\": true,\n    \"allOtherTripNotification\": true\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/flight/reserve/agent",
              "host": ["{{base_url}}"],
              "path": ["api", "flight", "reserve", "agent"]
            },
            "description": "Correct every returned traveler field before retrying. If the price changes, show it to the traveler before using the separate acceptance request. Third-party agent bookings never send customer reservation, unpaid-reservation reminder, or reservation-cancellation notifications. Final ticket notifications are enabled by default; set notifications.ticketPurchase to false to suppress them, and keep that value unchanged on retries."
          }
        },
        {
          "name": "4b. Accept approved changed price",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.environment.get('price_version') || !pm.environment.get('price_amount') || !pm.environment.get('price_currency')) {",
                  "  throw new Error('Run this request only after a changed price is returned and approved');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "if (pm.response.code >= 200 && pm.response.code < 300 && pm.response.code !== 202) {",
                  "  const reservation = body.reservation || {};",
                  "  const ticketId = reservation.ticketIds?.[0] || body.tickets?.[0]?.ticketId;",
                  "  pm.environment.set('reservation_id', reservation.reservationId);",
                  "  pm.environment.set('booking_reference', reservation.bookingReference);",
                  "  pm.environment.set('ticket_id', ticketId);",
                  "}",
                  "if (body.errorCode === 'FLIGHT_PRICE_CHANGED') {",
                  "  pm.environment.set('price_version', body.data.priceVersion);",
                  "  pm.environment.set('price_amount', body.data.currentPrice.grandTotal);",
                  "  pm.environment.set('price_currency', body.data.currentPrice.currency);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-External-User-Id",
                "value": "{{external_user_id}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"offerReference\": \"{{offer_reference}}\",\n  \"passengers\": [\n    {\n      \"firstName\": \"Asha\",\n      \"lastName\": \"Mushi\",\n      \"email\": \"asha.mushi@example.com\",\n      \"countryCode\": \"255\",\n      \"phoneNumber\": \"712345678\",\n      \"dateOfBirth\": \"1990-05-12\",\n      \"gender\": \"FEMALE\",\n      \"ageCategory\": \"ADULT\",\n      \"documentType\": \"PASSPORT\",\n      \"passportNumber\": \"TZ1234567\",\n      \"passportExpiryDate\": \"2030-05-12\",\n      \"documentIssuingCountry\": \"TZ\",\n      \"nationality\": \"TZ\"\n    }\n  ],\n  \"selectedOption\": \"NON_REFUNDABLE_FARE\",\n  \"notifications\": {\n    \"safiriAccountCreation\": true,\n    \"ticketPurchase\": true,\n    \"allOtherTripNotification\": true\n  },\n  \"priceAcceptance\": {\n    \"priceVersion\": \"{{price_version}}\",\n    \"amount\": \"{{price_amount}}\",\n    \"currency\": \"{{price_currency}}\"\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/flight/reserve/agent",
              "host": ["{{base_url}}"],
              "path": ["api", "flight", "reserve", "agent"]
            },
            "description": "Run only after showing the saved latest price to the traveler and receiving approval. If another price change is returned, obtain approval again before retrying. Keep notifications.ticketPurchase unchanged from the original reservation request."
          }
        },
        {
          "name": "5. Purchase reservation",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ticketId\": \"{{ticket_id}}\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/api/flight/purchase/agent",
              "host": ["{{base_url}}"],
              "path": ["api", "flight", "purchase", "agent"]
            },
            "description": "Purchase before paymentDeadlineAt. If the request is still being handled, wait briefly and send the same purchase request again. Successful purchase sends final ticket notifications by default unless the reservation set notifications.ticketPurchase to false."
          }
        },
        {
          "name": "6. Retrieve issued tickets",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/flight/tickets?ticketId={{ticket_id}}",
              "host": ["{{base_url}}"],
              "path": ["api", "flight", "tickets"],
              "query": [
                {
                  "key": "ticketId",
                  "value": "{{ticket_id}}"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Tickets",
      "item": [
        {
          "name": "Get all tickets",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/ticket",
              "host": ["{{base_url}}"],
              "path": ["api", "ticket"]
            }
          }
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "https://booking-api-dev.safiri.app",
      "type": "string"
    },
    {
      "key": "gtfs_flight_url",
      "value": "https://gtfs-flight.safiri.app",
      "type": "string"
    }
  ]
}
