api-hotel
Vue d'ensemble
Le service api-hotel gère l'ensemble de l'écosystème hôtelier de la plateforme Roomee. Il ne se limite pas aux hôtels physiques, mais gère tous les écosystèmes organisationnels (hotels, groupes, workspaces) avec leurs intégrations externes.
| Caractéristique | Détail |
|---|---|
| Port | 3002 |
| Stack | JavaScript (Legacy), Express.js, Prisma |
| Base de données | MongoDB (roomee_hotel) |
| Architecture | Legacy (JS + Repository Pattern) |
| Complexité | ⭐⭐⭐ |
Responsabilités
graph TB
HOTEL[api-hotel]
subgraph "Domaines"
ECOSYSTEM[Écosystèmes Hôteliers]
GROUPS[Groupes d'Hôtels]
SENDBIRD[Chat Sendbird]
STRIPE[Abonnements Stripe]
QR[QR Codes]
SETTINGS[Paramètres]
TYPES[Types d'Écosystème]
WEATHER[Météo]
end
HOTEL --> ECOSYSTEM
HOTEL --> GROUPS
HOTEL --> SENDBIRD
HOTEL --> STRIPE
HOTEL --> QR
HOTEL --> SETTINGS
HOTEL --> TYPES
HOTEL --> WEATHERStructure du Projet
api-hotel/
├── app.js # Configuration Express
├── loader.js # Démarrage serveur
├── controllers/
│ ├── hotel.controller.js # CRUD hôtels
│ ├── group.controller.js # Gestion groupes
│ ├── setting.controller.js # Paramètres
│ ├── type.controller.js # Types d'écosystème
│ ├── sendbird.controller.js # Intégration chat
│ └── weather.controller.js # Météo locale
├── routes/
│ ├── hotel.routes.js
│ ├── group.routes.js
│ ├── setting.routes.js
│ ├── type.routes.js
│ ├── sendbird.routes.js
│ └── weather.routes.js
├── repository/
│ ├── hotel.repository.js # Accès données Prisma
│ ├── group.repository.js
│ └── setting.repository.js
├── middlewares/
│ ├── auth.middleware.js # JWT validation
│ └── rateLimiter.middleware.js # Rate limiting
├── utils/
│ ├── ApiError.js
│ ├── catchAsync.js
│ ├── qrGenerator.js # Génération QR codes
│ └── sendbirdClient.js # Client Sendbird API
├── socket/
│ └── hotelSocket.js # Events temps réel
├── prisma/
│ └── schema.prisma # Schéma MongoDB
├── config/
│ └── config.js # Configuration
└── __tests/
└── hotel.test.jsModèles de Données
hotels
model hotels {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
address String?
city String?
country String?
phone String?
email String?
website String?
// Sendbird Integration
workspace String? // Workspace ID
sendbird_app_id String? @unique
sendbird_api_token String? // Encrypted
// QR Code
qr_link String? // URL du QR code (GCP)
// Stripe
stripe_customer_id String? @unique
subscription_status String? // active, inactive, trial
// Relations
groupId String? @db.ObjectId
groups groups? @relation(fields: [groupId], references: [id])
settingId String? @db.ObjectId
settings settings? @relation(fields: [settingId], references: [id])
typeId String? @db.ObjectId
type type? @relation(fields: [typeId], references: [id])
// Metadata
isActive Boolean @default(true)
deleted_at DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([groupId])
@@index([workspace])
@@index([sendbird_app_id])
}groups
model groups {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
description String?
logo_url String?
hotels hotels[]
deleted_at DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}settings
model settings {
id String @id @default(auto()) @map("_id") @db.ObjectId
// Paramètres généraux
timezone String @default("Europe/Paris")
language String @default("fr")
currency String @default("EUR")
// Paramètres de notification
enable_push Boolean @default(true)
enable_email Boolean @default(true)
enable_sms Boolean @default(false)
// Paramètres métier
check_in_time String @default("14:00")
check_out_time String @default("11:00")
// Personnalisation
primary_color String @default("#1E40AF")
secondary_color String @default("#10B981")
logo_url String?
hotels hotels[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}type
model type {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String @unique // "hotel", "resort", "hostel", "apartment"
description String?
icon String? // Icon name or emoji
hotels hotels[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Routes API
🏨 Hôtels (/hotel)
POST /hotel/add
Créer un nouvel écosystème hôtelier complet.
Headers :
Authorization: Bearer eyJhbGciOiJI...Request :
{
"name": "Grand Hotel Paris",
"address": "123 Rue de la Paix, 75002 Paris",
"city": "Paris",
"country": "France",
"phone": "+33 1 23 45 67 89",
"email": "contact@grandhotel-paris.fr",
"website": "https://grandhotel-paris.fr",
"groupId": "65f1234567890abcdef12345",
"typeId": "65f9876543210fedcba98765",
"settingId": "65f5555555555555555555555",
"administrator": {
"email": "admin@grandhotel-paris.fr",
"firstName": "Jean",
"lastName": "Dupont"
},
"subscription": {
"plan": "premium"
}
}Response :
{
"id": "65faaaaaaaaaaaaaaaaaaaaa",
"name": "Grand Hotel Paris",
"workspace": "65fbbbbbbbbbbbbbbbbbbbbb",
"sendbird_app_id": "A1B2C3D4E5",
"qr_link": "https://storage.googleapis.com/roomee/qr/65faa...png",
"stripe_customer_id": "cus_ABC123XYZ",
"subscription_status": "active",
"settings": {
"id": "65f5555555555555555555555",
"timezone": "Europe/Paris"
},
"type": {
"id": "65f9876543210fedcba98765",
"name": "hotel"
},
"administrator": {
"id": "65fccccccccccccccccccccc",
"email": "admin@grandhotel-paris.fr"
}
}Logique (Processus complet) :
- Validation des données d'entrée
- Création de l'hôtel dans la base
- Génération du QR code :javascript
const qrData = JSON.stringify({ hotelId: hotel.id, name: hotel.name, url: `https://app.roomee.io/hotel/${hotel.id}` }) const qrImage = await qrGenerator.generate(qrData) - Upload du QR vers GCP (api-media) :javascript
const qrUrl = await mediaService.uploadQR(hotel.id, qrImage) await hotelRepository.update(hotel.id, { qr_link: qrUrl }) - Création de l'application Sendbird :javascript
const sendbirdApp = await sendbirdClient.createApplication({ name: hotel.name, organization_id: process.env.SENDBIRD_ORG_ID }) await hotelRepository.update(hotel.id, { sendbird_app_id: sendbirdApp.app_id, sendbird_api_token: encrypt(sendbirdApp.api_token) }) - Création du workspace (base membres)
- Émission événement AMQP
storage.created→ api-media créer quota (20GB) - Création abonnement Stripe :javascript
const customer = await stripeService.createCustomer({ email: hotel.email, name: hotel.name, metadata: { hotelId: hotel.id } }) const subscription = await stripeService.createSubscription({ customer: customer.id, plan: 'premium' }) - Création de l'administrateur (api-staff-member) :javascript
await memberService.createHotelAdmin({ hotelId: hotel.id, ...administrator, dashboardAccess: true, isAdmin: true }) - Émission événement AMQP
hotel.created→ api-notification
GET /hotel/get/all
Récupérer tous les hôtels (pagination).
Query Params :
page: Numéro de page (défaut: 1)limit: Nombre d'éléments par page (défaut: 20)groupId: Filtrer par groupecity: Filtrer par villeisActive: Filtrer par statut
Response :
{
"data": [
{
"id": "65faaaaaaaaaaaaaaaaaaaaa",
"name": "Grand Hotel Paris",
"city": "Paris",
"country": "France",
"workspace": "65fbbbbbbbbbbbbbbbbbbbbb",
"sendbird_app_id": "A1B2C3D4E5",
"subscription_status": "active",
"group": {
"id": "65f1234567890abcdef12345",
"name": "Grand Hotels Group"
},
"type": {
"name": "hotel"
}
}
],
"meta": {
"total": 50,
"page": 1,
"limit": 20,
"totalPages": 3
}
}GET /hotel/get/:id
Récupérer un hôtel par son ID.
Response :
{
"id": "65faaaaaaaaaaaaaaaaaaaaa",
"name": "Grand Hotel Paris",
"address": "123 Rue de la Paix, 75002 Paris",
"city": "Paris",
"country": "France",
"phone": "+33 1 23 45 67 89",
"email": "contact@grandhotel-paris.fr",
"website": "https://grandhotel-paris.fr",
"workspace": "65fbbbbbbbbbbbbbbbbbbbbb",
"sendbird_app_id": "A1B2C3D4E5",
"qr_link": "https://storage.googleapis.com/roomee/qr/...",
"stripe_customer_id": "cus_ABC123XYZ",
"subscription_status": "active",
"group": {
"id": "65f1234567890abcdef12345",
"name": "Grand Hotels Group"
},
"settings": {
"timezone": "Europe/Paris",
"currency": "EUR",
"primary_color": "#1E40AF"
},
"type": {
"name": "hotel",
"icon": "🏨"
},
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-20T14:30:00.000Z"
}PATCH /hotel/update/:id
Mettre à jour un hôtel.
Request :
{
"name": "Grand Hotel Paris - Le Marais",
"phone": "+33 1 99 88 77 66",
"website": "https://grandhotel-lemarais.fr"
}Response :
{
"id": "65faaaaaaaaaaaaaaaaaaaaa",
"name": "Grand Hotel Paris - Le Marais",
"phone": "+33 1 99 88 77 66",
"updatedAt": "2025-01-21T09:15:00.000Z"
}Logique :
- Validation des données
- Mise à jour dans la base
- Émission événement AMQP
hotel.updated - Si changement de nom, mise à jour Sendbird app name
DELETE /hotel/delete/:id
Supprimer un hôtel (soft delete).
Response :
{
"message": "Hotel deleted successfully",
"id": "65faaaaaaaaaaaaaaaaaaaaa"
}Logique :
- Soft delete (
deleted_at = NOW()) - Récupération des membres du workspace (api-staff-member)
- Désactivation de tous les membres
- Annulation de l'abonnement Stripe
- Désactivation de l'app Sendbird
- Émission événement AMQP
hotel.deleted
📁 Groupes (/group)
POST /group/add
Créer un groupe d'hôtels.
Request :
{
"name": "Grand Hotels Group",
"description": "Chaîne d'hôtels de luxe en France",
"logo_url": "https://storage.googleapis.com/roomee/logos/group-logo.png"
}Response :
{
"id": "65f1234567890abcdef12345",
"name": "Grand Hotels Group",
"description": "Chaîne d'hôtels de luxe en France",
"logo_url": "https://storage.googleapis.com/roomee/logos/group-logo.png",
"createdAt": "2025-01-15T08:00:00.000Z"
}GET /group/get/all
Récupérer tous les groupes.
Response :
{
"data": [
{
"id": "65f1234567890abcdef12345",
"name": "Grand Hotels Group",
"hotelsCount": 15,
"logo_url": "https://storage.googleapis.com/...",
"createdAt": "2025-01-15T08:00:00.000Z"
}
]
}GET /group/get/:id
Récupérer un groupe avec ses hôtels.
Response :
{
"id": "65f1234567890abcdef12345",
"name": "Grand Hotels Group",
"description": "Chaîne d'hôtels de luxe en France",
"logo_url": "https://storage.googleapis.com/...",
"hotels": [
{
"id": "65faaaaaaaaaaaaaaaaaaaaa",
"name": "Grand Hotel Paris",
"city": "Paris",
"subscription_status": "active"
},
{
"id": "65fdddddddddddddddddddd",
"name": "Grand Hotel Lyon",
"city": "Lyon",
"subscription_status": "active"
}
],
"createdAt": "2025-01-15T08:00:00.000Z"
}⚙️ Paramètres (/setting)
POST /setting/add
Créer un profil de paramètres.
Request :
{
"timezone": "Europe/Paris",
"language": "fr",
"currency": "EUR",
"check_in_time": "15:00",
"check_out_time": "12:00",
"enable_push": true,
"enable_email": true,
"primary_color": "#0F172A",
"secondary_color": "#3B82F6"
}Response :
{
"id": "65f5555555555555555555555",
"timezone": "Europe/Paris",
"language": "fr",
"currency": "EUR",
"check_in_time": "15:00",
"check_out_time": "12:00",
"primary_color": "#0F172A",
"secondary_color": "#3B82F6"
}GET /setting/get/all
Récupérer tous les profils de paramètres.
PATCH /setting/update/:id
Mettre à jour des paramètres.
🏷️ Types (/type)
POST /type/add
Créer un type d'écosystème.
Request :
{
"name": "resort",
"description": "Station balnéaire ou de montagne",
"icon": "🏖️"
}GET /type/get/all
Récupérer tous les types disponibles.
Response :
{
"data": [
{ "id": "...", "name": "hotel", "icon": "🏨" },
{ "id": "...", "name": "resort", "icon": "🏖️" },
{ "id": "...", "name": "hostel", "icon": "🏠" },
{ "id": "...", "name": "apartment", "icon": "🏢" }
]
}💬 Sendbird (/sendbird)
POST /sendbird/create-app
Créer une application Sendbird pour un hôtel.
Request :
{
"hotelId": "65faaaaaaaaaaaaaaaaaaaaa",
"name": "Grand Hotel Paris Chat"
}Response :
{
"app_id": "A1B2C3D4E5",
"api_token": "encrypted_token",
"application_name": "Grand Hotel Paris Chat",
"region": "eu-1"
}Logique :
const sendbirdApp = await axios.post(
'https://api-{org_id}.sendbird.com/v3/applications',
{
organization_id: process.env.SENDBIRD_ORG_ID,
name: hotelName,
region: 'eu-1'
},
{
headers: {
'Api-Token': process.env.SENDBIRD_ORGANIZATION_API_TOKEN
}
}
)GET /sendbird/stats/:hotelId
Récupérer les statistiques Sendbird d'un hôtel.
Response :
{
"users_count": 250,
"channels_count": 45,
"messages_today": 1250,
"active_users_today": 120
}☀️ Météo (/weather)
GET /weather/:city
Récupérer la météo d'une ville.
Response :
{
"city": "Paris",
"country": "FR",
"temperature": 22,
"feels_like": 20,
"humidity": 65,
"description": "Partly cloudy",
"icon": "02d",
"wind_speed": 5.2,
"forecast": [
{
"date": "2025-01-21",
"temp_max": 24,
"temp_min": 18,
"description": "Sunny"
},
{
"date": "2025-01-22",
"temp_max": 22,
"temp_min": 16,
"description": "Rainy"
}
]
}Intégration OpenWeather :
const weather = await axios.get(
'https://api.openweathermap.org/data/2.5/weather',
{
params: {
q: city,
appid: process.env.OPENWEATHER_API_KEY,
units: 'metric',
lang: 'fr'
}
}
)Intégrations Externes
Sendbird Chat
// Client Sendbird
const sendbirdClient = {
baseURL: 'https://api-{app_id}.sendbird.com/v3',
async createApplication(data) {
return await axios.post('/applications', data, {
headers: { 'Api-Token': orgToken }
})
},
async createUser(appId, userId, nickname) {
return await axios.post(`/${appId}/users`, {
user_id: userId,
nickname: nickname
}, {
headers: { 'Api-Token': appToken }
})
},
async createChannel(appId, userIds) {
return await axios.post(`/${appId}/group_channels`, {
user_ids: userIds,
is_distinct: true
}, {
headers: { 'Api-Token': appToken }
})
}
}Stripe Subscriptions
const stripeService = {
async createCustomer(data) {
return await stripe.customers.create({
email: data.email,
name: data.name,
metadata: { hotelId: data.hotelId }
})
},
async createSubscription(customerId, planId) {
return await stripe.subscriptions.create({
customer: customerId,
items: [{ price: planId }],
trial_period_days: 14 // 14 jours d'essai
})
},
async cancelSubscription(subscriptionId) {
return await stripe.subscriptions.cancel(subscriptionId)
}
}api-staff-member
// Créer administrateur hôtel
await axios.post(
`${MEMBER_API_URL}/members/add/hotel/member`,
{
hotelId: hotel.id,
email: admin.email,
firstName: admin.firstName,
lastName: admin.lastName,
dashboardAccess: true,
isAdmin: true
},
{
headers: {
'X-API-Key': process.env.MEMBER_SERVICE_API_KEY
}
}
)
// Récupérer membres lors de la suppression
const members = await axios.get(
`${MEMBER_API_URL}/members/get/by/hotel_id/${hotelId}`,
{
headers: {
'X-API-Key': process.env.MEMBER_SERVICE_API_KEY
}
}
)api-media (QR Codes)
// Upload QR code
const formData = new FormData()
formData.append('file', qrImageBuffer)
formData.append('folder', 'qr-codes')
formData.append('ecosystemId', hotelId)
const response = await axios.post(
`${MEDIA_API_URL}/media/upload`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
'X-API-Key': process.env.MEDIA_SERVICE_API_KEY
}
}
)
return response.data.fileUrlÉvénements AMQP
Événements Émis
| Routing Key | Payload | Description |
|---|---|---|
roomee.notification.hotel.created | { hotelId, name, adminId } | Hôtel créé |
roomee.notification.hotel.updated | { hotelId, changes } | Hôtel mis à jour |
roomee.notification.hotel.deleted | { hotelId, name } | Hôtel supprimé |
roomee.storage.media | { ecosystemId, max_size, unit } | Configuration storage |
Exemple :
await amqpServer.emit('roomee.storage.media', {
ecosystemId: hotel.id,
payload: {
max_size: 20000, // 20GB pour Premium
unit: 'MO'
}
})Socket.IO Events
// hotelSocket.js
io.on('connection', (socket) => {
// Rejoindre la room de l'hôtel
socket.on('joinHotel', (hotelId) => {
socket.join(`hotel:${hotelId}`)
})
// Notifier changement de paramètres
socket.on('settingsUpdated', (data) => {
io.to(`hotel:${data.hotelId}`).emit('settings:updated', data)
})
})
// Émission serveur
io.to(`hotel:${hotelId}`).emit('subscription:status', {
status: 'active',
plan: 'premium'
})Configuration
Variables d'Environnement
# Server
PORT=3002
NODE_ENV=development
# Database
DATABASE_URL=mongodb://localhost:27017/roomee_hotel
# JWT
JWT_ACCESS_SECRET=your-jwt-secret
# Sendbird
SENDBIRD_ORGANIZATION_API_TOKEN=your-sendbird-org-token
SENDBIRD_ORG_ID=your-organization-id
# Stripe
STRIPE_SECRET_KEY=sk_test_your_stripe_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
# OpenWeather
OPENWEATHER_API_KEY=your-openweather-key
# Service URLs
MEMBER_API_URL=http://localhost:3001
MEDIA_API_URL=http://localhost:3003
# API Keys
MEMBER_SERVICE_API_KEY=your-member-api-key
MEDIA_SERVICE_API_KEY=your-media-api-key
# AMQP
AMQP_GATEWAY_URL=amqp://localhost:5672
AMQP_EXCHANGE_NAME=roomee_events
AMQP_ROUTING_KEY_BASE=roomee.notification.hotelTests
// __tests__/hotel.test.js
describe('HotelController', () => {
describe('createHotel', () => {
it('should create a complete hotel ecosystem', async () => {
const hotelData = {
name: 'Test Hotel',
email: 'test@hotel.com',
administrator: {
email: 'admin@hotel.com',
firstName: 'Admin',
lastName: 'Test'
}
}
const hotel = await hotelController.create(hotelData)
expect(hotel).toHaveProperty('id')
expect(hotel).toHaveProperty('workspace')
expect(hotel).toHaveProperty('sendbird_app_id')
expect(hotel).toHaveProperty('qr_link')
expect(hotel).toHaveProperty('stripe_customer_id')
})
})
})Bonnes Pratiques
1. Toujours vérifier les relations
// Avant de créer un hôtel
if (groupId) {
const group = await groupRepository.findById(groupId)
if (!group) throw new ApiError(404, 'Group not found')
}2. Gérer les transactions externes
// Rollback si une étape échoue
try {
const hotel = await createHotel(data)
const sendbirdApp = await createSendbirdApp(hotel)
const subscription = await createStripeSubscription(hotel)
} catch (error) {
if (hotel) await deleteHotel(hotel.id)
if (sendbirdApp) await deleteSendbirdApp(sendbirdApp.app_id)
throw error
}3. Chiffrer les secrets
import { encryptionService } from '@roomee/shared'
// Chiffrer API token Sendbird
const encrypted = encryptionService.encrypt(apiToken)
await hotelRepository.update(id, {
sendbird_api_token: encrypted
})
// Déchiffrer pour utilisation
const decrypted = encryptionService.decrypt(hotel.sendbird_api_token)Améliorations Futures
- [ ] Cache Redis pour les settings (éviter DB calls)
- [ ] Webhooks Stripe pour sync subscriptions
- [ ] Multi-région Sendbird
- [ ] Analytics dashboard intégré
- [ ] Gestion des reviews/ratings
- [ ] Intégration PMS (Property Management System)