{ "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" } ] }