{
  "info": {
    "_postman_id": "xpeditis-api-collection-v2",
    "name": "Xpeditis API - Maritime Freight Booking (Phase 2)",
    "description": "Collection complète pour tester l'API Xpeditis - Plateforme de réservation de fret maritime B2B\n\n**Base URL:** http://localhost:4000\n\n**Fonctionnalités:**\n- 🔐 Authentication JWT (register, login, refresh token)\n- 📊 Recherche de tarifs maritimes multi-transporteurs\n- 📦 Création et gestion de réservations\n- ✅ Validation automatique des données\n- ⚡ Cache Redis (15 min)\n\n**Phase actuelle:** MVP Phase 2 - Authentication & User Management\n\n**Important:** \n1. Commencez par créer un compte (POST /auth/register)\n2. Ensuite connectez-vous (POST /auth/login) pour obtenir un token JWT\n3. Le token sera automatiquement ajouté aux autres requêtes\n4. Le token expire après 15 minutes (utilisez /auth/refresh pour en obtenir un nouveau)",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{accessToken}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "Authentication",
      "description": "Endpoints d'authentification JWT : register, login, refresh token, logout, profil utilisateur",
      "item": [
        {
          "name": "Register New User",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test(\"Status code is 201 (Created)\", function () {",
                  "    pm.response.to.have.status(201);",
                  "});",
                  "",
                  "pm.test(\"Response has access and refresh tokens\", function () {",
                  "    var jsonData = pm.response.json();",
                  "    pm.expect(jsonData).to.have.property('accessToken');",
                  "    pm.expect(jsonData).to.have.property('refreshToken');",
                  "    pm.expect(jsonData).to.have.property('user');",
                  "});",
                  "",
                  "pm.test(\"User object has correct properties\", function () {",
                  "    var user = pm.response.json().user;",
                  "    pm.expect(user).to.have.property('id');",
                  "    pm.expect(user).to.have.property('email');",
                  "    pm.expect(user).to.have.property('role');",
                  "    pm.expect(user).to.have.property('organizationId');",
                  "});",
                  "",
                  "// Save tokens for subsequent requests",
                  "if (pm.response.code === 201) {",
                  "    var jsonData = pm.response.json();",
                  "    pm.environment.set(\"accessToken\", jsonData.accessToken);",
                  "    pm.environment.set(\"refreshToken\", jsonData.refreshToken);",
                  "    pm.environment.set(\"userId\", jsonData.user.id);",
                  "    pm.environment.set(\"userEmail\", jsonData.user.email);",
                  "    console.log(\"✅ Registration successful! Tokens saved.\");",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"john.doe@acme.com\",\n  \"password\": \"SecurePassword123!\",\n  \"firstName\": \"John\",\n  \"lastName\": \"Doe\",\n  \"organizationId\": \"550e8400-e29b-41d4-a716-446655440000\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/auth/register",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "register"]
            },
            "description": "Créer un nouveau compte utilisateur\n\n**Validation:**\n- Email format valide\n- Password minimum 12 caractères\n- FirstName et LastName minimum 2 caractères\n- OrganizationId format UUID\n\n**Réponse:**\n- accessToken (expire après 15 min)\n- refreshToken (expire après 7 jours)\n- user object avec id, email, role, organizationId\n\n**Sécurité:**\n- Password hashé avec Argon2id (64MB memory, 3 iterations)\n- JWT signé avec HS256"
          },
          "response": []
        },
        {
          "name": "Login",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test(\"Response has tokens\", function () {",
                  "    var jsonData = pm.response.json();",
                  "    pm.expect(jsonData).to.have.property('accessToken');",
                  "    pm.expect(jsonData).to.have.property('refreshToken');",
                  "});",
                  "",
                  "// Save tokens",
                  "if (pm.response.code === 200) {",
                  "    var jsonData = pm.response.json();",
                  "    pm.environment.set(\"accessToken\", jsonData.accessToken);",
                  "    pm.environment.set(\"refreshToken\", jsonData.refreshToken);",
                  "    pm.environment.set(\"userId\", jsonData.user.id);",
                  "    console.log(\"✅ Login successful! Access token expires in 15 minutes.\");",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"john.doe@acme.com\",\n  \"password\": \"SecurePassword123!\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/auth/login",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "login"]
            },
            "description": "Se connecter avec email et password\n\n**Réponse:**\n- accessToken (15 min)\n- refreshToken (7 jours)\n- user info\n\n**Erreurs possibles:**\n- 401: Email ou password incorrect\n- 401: Compte inactif"
          },
          "response": []
        },
        {
          "name": "Refresh Access Token",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test(\"Response has new access token\", function () {",
                  "    var jsonData = pm.response.json();",
                  "    pm.expect(jsonData).to.have.property('accessToken');",
                  "});",
                  "",
                  "// Update access token",
                  "if (pm.response.code === 200) {",
                  "    pm.environment.set(\"accessToken\", pm.response.json().accessToken);",
                  "    console.log(\"✅ Access token refreshed successfully!\");",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"refreshToken\": \"{{refreshToken}}\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/auth/refresh",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "refresh"]
            },
            "description": "Obtenir un nouveau access token avec le refresh token\n\n**Cas d'usage:**\n- Access token expiré (après 15 min)\n- Refresh token valide (< 7 jours)\n\n**Réponse:**\n- Nouveau accessToken valide pour 15 min\n\n**Note:** Le refresh token reste inchangé"
          },
          "response": []
        },
        {
          "name": "Get Current User Profile",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test(\"Response has user profile\", function () {",
                  "    var jsonData = pm.response.json();",
                  "    pm.expect(jsonData).to.have.property('id');",
                  "    pm.expect(jsonData).to.have.property('email');",
                  "    pm.expect(jsonData).to.have.property('role');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/auth/me",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "me"]
            },
            "description": "Récupérer le profil de l'utilisateur connecté\n\n**Authentification:** Requiert un access token valide\n\n**Réponse:**\n- id (UUID)\n- email\n- firstName\n- lastName\n- role (admin, manager, user, viewer)\n- organizationId"
          },
          "response": []
        },
        {
          "name": "Logout",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "// Clear tokens from environment",
                  "pm.environment.unset(\"accessToken\");",
                  "pm.environment.unset(\"refreshToken\");",
                  "console.log(\"✅ Logged out successfully. Tokens cleared.\");"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/auth/logout",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "logout"]
            },
            "description": "Déconnecter l'utilisateur\n\n**Note:** Avec JWT, la déconnexion est principalement gérée côté client en supprimant les tokens. Pour plus de sécurité, une blacklist Redis peut être implémentée."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Rates API",
      "description": "Recherche de tarifs maritimes auprès de plusieurs transporteurs (Maersk, MSC, CMA CGM, etc.)",
      "item": [
        {
          "name": "Search Rates - Rotterdam to Shanghai",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test(\"Response has quotes array\", function () {",
                  "    var jsonData = pm.response.json();",
                  "    pm.expect(jsonData).to.have.property('quotes');",
                  "    pm.expect(jsonData.quotes).to.be.an('array');",
                  "});",
                  "",
                  "pm.test(\"Response time is acceptable\", function () {",
                  "    pm.expect(pm.response.responseTime).to.be.below(3000);",
                  "});",
                  "",
                  "// Save first quote ID for booking tests",
                  "if (pm.response.json().quotes.length > 0) {",
                  "    pm.environment.set(\"rateQuoteId\", pm.response.json().quotes[0].id);",
                  "    console.log(\"Saved rateQuoteId: \" + pm.response.json().quotes[0].id);",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"origin\": \"NLRTM\",\n  \"destination\": \"CNSHA\",\n  \"containerType\": \"40HC\",\n  \"mode\": \"FCL\",\n  \"departureDate\": \"2025-02-15\",\n  \"quantity\": 2,\n  \"weight\": 20000,\n  \"isHazmat\": false\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/rates/search",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "rates", "search"]
            },
            "description": "🔐 **Authentification requise**\n\nRecherche de tarifs maritimes pour Rotterdam → Shanghai\n\n**Paramètres:**\n- `origin`: Code UN/LOCODE (5 caractères) - NLRTM = Rotterdam\n- `destination`: Code UN/LOCODE - CNSHA = Shanghai\n- `containerType`: 40HC (40ft High Cube)\n- `mode`: FCL (Full Container Load)\n- `departureDate`: Date de départ souhaitée\n- `quantity`: Nombre de conteneurs\n- `weight`: Poids total en kg\n\n**Cache:** Résultats mis en cache pendant 15 minutes"
          },
          "response": []
        }
      ]
    },
    {
      "name": "Bookings API",
      "description": "Gestion complète des réservations : création, consultation, listing",
      "item": [
        {
          "name": "Create Booking",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test(\"Status code is 201 (Created)\", function () {",
                  "    pm.response.to.have.status(201);",
                  "});",
                  "",
                  "pm.test(\"Response has booking ID\", function () {",
                  "    var jsonData = pm.response.json();",
                  "    pm.expect(jsonData).to.have.property('id');",
                  "    pm.expect(jsonData).to.have.property('bookingNumber');",
                  "});",
                  "",
                  "pm.test(\"Booking number has correct format\", function () {",
                  "    var jsonData = pm.response.json();",
                  "    pm.expect(jsonData.bookingNumber).to.match(/^WCM-\\d{4}-[A-Z0-9]{6}$/);",
                  "});",
                  "",
                  "// Save booking ID and number",
                  "pm.environment.set(\"bookingId\", pm.response.json().id);",
                  "pm.environment.set(\"bookingNumber\", pm.response.json().bookingNumber);",
                  "console.log(\"Saved bookingId: \" + pm.response.json().id);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// Ensure we have a rateQuoteId",
                  "if (!pm.environment.get(\"rateQuoteId\")) {",
                  "    console.warn(\"⚠️ No rateQuoteId found. Run 'Search Rates' first!\");",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rateQuoteId\": \"{{rateQuoteId}}\",\n  \"shipper\": {\n    \"name\": \"Acme Corporation\",\n    \"address\": {\n      \"street\": \"123 Main Street\",\n      \"city\": \"Rotterdam\",\n      \"postalCode\": \"3000 AB\",\n      \"country\": \"NL\"\n    },\n    \"contactName\": \"John Doe\",\n    \"contactEmail\": \"john.doe@acme.com\",\n    \"contactPhone\": \"+31612345678\"\n  },\n  \"consignee\": {\n    \"name\": \"Shanghai Imports Ltd\",\n    \"address\": {\n      \"street\": \"456 Trade Avenue\",\n      \"city\": \"Shanghai\",\n      \"postalCode\": \"200000\",\n      \"country\": \"CN\"\n    },\n    \"contactName\": \"Jane Smith\",\n    \"contactEmail\": \"jane.smith@shanghai-imports.cn\",\n    \"contactPhone\": \"+8613812345678\"\n  },\n  \"cargoDescription\": \"Electronics and consumer goods for retail distribution\",\n  \"containers\": [\n    {\n      \"type\": \"40HC\",\n      \"containerNumber\": \"ABCU1234567\",\n      \"vgm\": 22000,\n      \"sealNumber\": \"SEAL123456\"\n    }\n  ],\n  \"specialInstructions\": \"Please handle with care. Delivery before 5 PM.\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/bookings",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "bookings"]
            },
            "description": "🔐 **Authentification requise**\n\nCréer une nouvelle réservation basée sur un tarif recherché\n\n**Note:** La réservation sera automatiquement liée à l'utilisateur et l'organisation connectés."
          },
          "response": []
        },
        {
          "name": "Get Booking by ID",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/bookings/{{bookingId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "bookings", "{{bookingId}}"]
            },
            "description": "🔐 **Authentification requise**\n\nRécupérer les détails d'une réservation par ID\n\n**Sécurité:** Seules les réservations de votre organisation sont accessibles"
          },
          "response": []
        },
        {
          "name": "List Bookings (Paginated)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/bookings?page=1&pageSize=20",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "bookings"],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "pageSize",
                  "value": "20"
                },
                {
                  "key": "status",
                  "value": "draft",
                  "disabled": true
                }
              ]
            },
            "description": "🔐 **Authentification requise**\n\nLister toutes les réservations de votre organisation\n\n**Filtrage automatique:** Seules les réservations de votre organisation sont affichées"
          },
          "response": []
        }
      ]
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Check if access token exists and warn if missing (except for auth endpoints)",
          "const url = pm.request.url.toString();",
          "const isAuthEndpoint = url.includes('/auth/');",
          "",
          "if (!isAuthEndpoint && !pm.environment.get('accessToken')) {",
          "    console.warn('⚠️ No access token found. Please login first!');",
          "}"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Global test: check for 401 and suggest refresh",
          "if (pm.response.code === 401) {",
          "    console.error('❌ Unauthorized (401). Your token may have expired.');",
          "    console.log('💡 Try refreshing your access token with POST /auth/refresh');",
          "}"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:4000",
      "type": "string"
    },
    {
      "key": "accessToken",
      "value": "",
      "type": "string"
    },
    {
      "key": "refreshToken",
      "value": "",
      "type": "string"
    },
    {
      "key": "userId",
      "value": "",
      "type": "string"
    },
    {
      "key": "userEmail",
      "value": "",
      "type": "string"
    },
    {
      "key": "rateQuoteId",
      "value": "",
      "type": "string"
    },
    {
      "key": "bookingId",
      "value": "",
      "type": "string"
    },
    {
      "key": "bookingNumber",
      "value": "",
      "type": "string"
    }
  ]
}
