Skip to content

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éristiqueDétail
Port3002
StackJavaScript (Legacy), Express.js, Prisma
Base de donnéesMongoDB (roomee_hotel)
ArchitectureLegacy (JS + Repository Pattern)
Complexité⭐⭐⭐

Responsabilités

mermaid
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 --> WEATHER

Structure 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.js

Modèles de Données

hotels

prisma
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

prisma
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

prisma
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

prisma
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 :

json
{
  "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 :

json
{
  "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) :

  1. Validation des données d'entrée
  2. Création de l'hôtel dans la base
  3. 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)
  4. Upload du QR vers GCP (api-media) :
    javascript
    const qrUrl = await mediaService.uploadQR(hotel.id, qrImage)
    await hotelRepository.update(hotel.id, { qr_link: qrUrl })
  5. 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)
    })
  6. Création du workspace (base membres)
  7. Émission événement AMQP storage.created → api-media créer quota (20GB)
  8. 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'
    })
  9. Création de l'administrateur (api-staff-member) :
    javascript
    await memberService.createHotelAdmin({
      hotelId: hotel.id,
      ...administrator,
      dashboardAccess: true,
      isAdmin: true
    })
  10. É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 groupe
  • city : Filtrer par ville
  • isActive : Filtrer par statut

Response :

json
{
  "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 :

json
{
  "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 :

json
{
  "name": "Grand Hotel Paris - Le Marais",
  "phone": "+33 1 99 88 77 66",
  "website": "https://grandhotel-lemarais.fr"
}

Response :

json
{
  "id": "65faaaaaaaaaaaaaaaaaaaaa",
  "name": "Grand Hotel Paris - Le Marais",
  "phone": "+33 1 99 88 77 66",
  "updatedAt": "2025-01-21T09:15:00.000Z"
}

Logique :

  1. Validation des données
  2. Mise à jour dans la base
  3. Émission événement AMQP hotel.updated
  4. Si changement de nom, mise à jour Sendbird app name

DELETE /hotel/delete/:id

Supprimer un hôtel (soft delete).

Response :

json
{
  "message": "Hotel deleted successfully",
  "id": "65faaaaaaaaaaaaaaaaaaaaa"
}

Logique :

  1. Soft delete (deleted_at = NOW())
  2. Récupération des membres du workspace (api-staff-member)
  3. Désactivation de tous les membres
  4. Annulation de l'abonnement Stripe
  5. Désactivation de l'app Sendbird
  6. Émission événement AMQP hotel.deleted

📁 Groupes (/group)

POST /group/add

Créer un groupe d'hôtels.

Request :

json
{
  "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 :

json
{
  "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 :

json
{
  "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 :

json
{
  "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 :

json
{
  "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 :

json
{
  "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 :

json
{
  "name": "resort",
  "description": "Station balnéaire ou de montagne",
  "icon": "🏖️"
}

GET /type/get/all

Récupérer tous les types disponibles.

Response :

json
{
  "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 :

json
{
  "hotelId": "65faaaaaaaaaaaaaaaaaaaaa",
  "name": "Grand Hotel Paris Chat"
}

Response :

json
{
  "app_id": "A1B2C3D4E5",
  "api_token": "encrypted_token",
  "application_name": "Grand Hotel Paris Chat",
  "region": "eu-1"
}

Logique :

javascript
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 :

json
{
  "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 :

json
{
  "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 :

javascript
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

javascript
// 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

javascript
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

javascript
// 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)

javascript
// 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 KeyPayloadDescription
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 :

javascript
await amqpServer.emit('roomee.storage.media', {
  ecosystemId: hotel.id,
  payload: {
    max_size: 20000, // 20GB pour Premium
    unit: 'MO'
  }
})

Socket.IO Events

javascript
// 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

bash
# 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.hotel

Tests

javascript
// __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

javascript
// 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

javascript
// 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

javascript
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)

Documentation technique Roomee Services