Skip to content

Interactions entre Services ​

Cette page décrit en détail comment les services Roomee communiquent entre eux, avec des exemples concrets de flux métier.

Types de Communication ​

1. HTTP REST (Synchrone) ​

Communication directe pour opérations nécessitant une réponse immédiate.

typescript
// Exemple : api-hotel appelle api-staff-member
const response = await axios.post(
  `${MEMBER_API_URL}/members/add/hotel/member`,
  memberData,
  { headers: { Authorization: `Bearer ${token}` } }
)

2. AMQP (Asynchrone) ​

Événements métier pour déclencher des actions dans d'autres services.

typescript
// Exemple : api-news émet un événement
await amqpServer.emit('roomee.notification.page.created', {
  type: 'page.created',
  workspaceId: 'xyz',
  payload: pageData
})

3. Socket.IO (Temps Réel) ​

Mises à jour temps réel pour l'interface utilisateur.

typescript
// Exemple : api-notification émet via Socket.IO
io.to(`workspace:${workspaceId}`).emit('notification:new', notification)

Flux Métier Détaillés ​

Flux 1 : Création d'un Écosystème Hôtelier ​

Services impliqués : api-hotel, api-media, api-stripe, api-staff-member, api-notification

mermaid
sequenceDiagram
    participant Admin
    participant Hotel as api-hotel
    participant Media as api-media
    participant Stripe as api-stripe
    participant Member as api-staff-member
    participant Notif as api-notification
    participant AMQP

    Admin->>Hotel: POST /hotel/add
    Hotel->>Hotel: Create hotel in DB
    Hotel->>Media: POST /media/upload (QR code)
    Media-->>Hotel: QR URL
    Hotel->>Hotel: Update hotel.qr_link
    Hotel->>AMQP: Emit storage.created
    Media-->>Media: Create storage quota
    Hotel->>Stripe: POST /sub/assign-subscription
    Stripe-->>Hotel: Subscription data
    Hotel->>Member: POST /members/add/hotel/member
    Member-->>Hotel: Admin member
    Hotel->>AMQP: Emit hotel.created
    AMQP->>Notif: Consume event
    Notif->>Notif: Create notification
    Notif->>Admin: Push + Socket notification

Détails techniques :

  1. Création de l'hôtel (api-hotel)

    typescript
    const hotel = await prisma.hotels.create({
      data: {
        name, email, phone, address,
        settings: { connect: { id: settingId } },
        type: { connect: { id: typeId } }
      }
    })
  2. Génération QR code (api-media)

    typescript
    // Upload image PNG vers GCP
    const qrUrl = await uploadQRCode(hotelId, qrImage)
  3. Configuration stockage (AMQP → api-media)

    typescript
    await amqpServer.emit('roomee.storage.media', {
      ecosystemId: hotel.id,
      payload: {
        max_size: 20000, // 20GB pour Premium
        unit: 'MO'
      }
    })
  4. Abonnement Stripe (api-stripe)

    typescript
    await axios.post('/sub/assign-subscription/', {
      customer_id: hotel.id,
      product_id: 'premium'
    })
  5. Création administrateur (api-staff-member)

    typescript
    await axios.post('/members/add/hotel/member', {
      ...administrator,
      hotelId: hotel.id,
      dashboardAccess: true,
      isAdmin: true
    })

Flux 2 : Publication d'un Article ​

Services impliqués : api-news, api-staff-member, api-notification

mermaid
sequenceDiagram
    participant User
    participant News as api-news
    participant Member as api-staff-member
    participant Notif as api-notification
    participant AMQP

    User->>News: POST /api/v1/pages (article)
    News->>News: Create page in DB
    News->>Member: Get workspace members
    Member-->>News: Member list
    News->>AMQP: Emit page.created
    AMQP->>Notif: Consume event
    Notif->>Notif: Filter by preferences
    Notif->>Notif: Create notification
    Notif->>Notif: Dispatch (Push + Socket)
    Notif->>User: Push notification
    Notif->>User: Socket update

Détails du payload AMQP :

typescript
{
  type: 'page.created',
  workspaceId: 'abc123',
  ecosystemId: 'hotel456',
  workspaceName: 'Mon Workspace',
  payload: {
    id: 'page789',
    title: 'Nouvelle politique RH',
    parent: {
      id: 'feed123',
      title: 'Actualités',
      pageType: 'feed'
    },
    medias: [{ fileUrl: '...', thumbnailUrl: '...' }]
  },
  channels: ['push', 'socket'],
  metadata: {
    timestamp: '2025-10-31T12:00:00.000Z',
    actor: {
      id: 'member123',
      name: 'John Doe',
      type: 'member'
    },
    receiverIds: ['member456', 'member789']
  }
}

Flux 3 : Ajout d'un Commentaire ​

Services impliqués : api-news, api-notification

mermaid
sequenceDiagram
    participant User
    participant News as api-news
    participant Notif as api-notification
    participant AMQP
    participant Author
    participant Members

    User->>News: POST /api/v1/comments
    News->>News: Create comment in DB
    News->>News: Update page.commentCount
    News->>AMQP: Emit comment.created (2 events)

    Note over AMQP,Notif: Event 1: Socket only (tous)
    AMQP->>Notif: channels: ['socket']
    Notif->>Members: Socket update (sauf auteur)

    Note over AMQP,Notif: Event 2: Push + Socket (auteur)
    AMQP->>Notif: channels: ['push', 'socket']
    Notif->>Notif: Create notification
    Notif->>Author: Push notification
    Notif->>Author: Socket update

Pattern de notification graduée :

  1. Tous les membres du workspace → Socket uniquement (mise à jour UI)
  2. Auteur de la page → Push + Socket (notification complète)
typescript
// api-news émet 2 événements
// 1. Socket pour tous
await pageEventService.emitCreatedEvent(page, req, false)

// 2. Push + Socket pour l'auteur
if (page.enableNotifications) {
  await pageEventService.emitCreatedEvent(page, req, true)
}

Flux 4 : Invitation d'un Nouveau Membre ​

Services impliqués : api-staff-member, api-authentication, api-notification

mermaid
sequenceDiagram
    participant Manager
    participant Member as api-staff-member
    participant Auth as api-authentication
    participant Notif as api-notification
    participant AMQP
    participant NewMember

    Manager->>Member: POST /members/ (invitation)
    Member->>Member: Create onboarding record
    Member->>Member: Generate invitation token
    Member->>AMQP: Emit member.onboarding
    AMQP->>Notif: Consume event
    Notif->>Notif: Create notification
    Notif->>NewMember: Email invitation
    Notif->>Manager: Socket update

    Note over NewMember,Auth: Acceptation
    NewMember->>Auth: POST /users/signUp (with token)
    Auth->>Auth: Create user account
    Auth->>Member: Create member profile
    Auth->>AMQP: Emit member.registered
    AMQP->>Notif: Consume event
    Notif->>Manager: Notification membre inscrit

Flux 5 : Upload de Média ​

Services impliqués : api-media, api-news

mermaid
sequenceDiagram
    participant User
    participant News as api-news
    participant Media as api-media
    participant GCP

    User->>Media: POST /media/generate-presigned-url
    Media->>GCP: Generate signed URL (15min)
    GCP-->>Media: Signed URL
    Media-->>User: URL + metadata

    User->>GCP: PUT (direct upload)
    GCP-->>User: Upload success

    User->>Media: POST /media/upload-complete
    Media->>GCP: Get file metadata
    Media->>Media: Generate thumbnail
    Media->>GCP: Upload thumbnail
    Media-->>User: fileUrl + thumbnailUrl

    User->>News: POST /api/v1/pages (with media)
    News->>News: Create page with medias[]

Avantages de l'upload direct :

  • ✅ Pas de transit par le serveur
  • ✅ Meilleure performance
  • ✅ Scalabilité
  • ✅ Économie de bande passante

Flux 6 : Notification Multi-Canal ​

Service : api-notification (consumer AMQP)

mermaid
graph TB
    AMQP[Event AMQP] --> Consumer[EventConsumer]
    Consumer --> Handler[EventHandler]
    Handler --> Processor[NotificationProcessor]

    Processor --> Factory[NotificationFactory]
    Factory --> Template[Get Template]
    Template --> Render[Render with variables]

    Processor --> Filter[Filter by preferences]
    Filter --> Save[Save notification_receiver]

    Processor --> Dispatcher[NotificationDispatcher]

    Dispatcher --> Socket[SocketDispatcher]
    Dispatcher --> Push[PushDispatcher]
    Dispatcher --> Email[EmailDispatcher]

    Socket --> SocketIO[Socket.IO emit]
    Push --> OneSignal[OneSignal API]
    Email --> Nodemailer[Nodemailer + SendGrid]

    Dispatcher --> Counters[Update unread counters]
    Dispatcher --> Attempts[Save delivery_attempts]

Logique de filtrage :

  1. Préférences utilisateur (notification_preference)

    typescript
    if (userPrefs.isGlobalSilent) return false
    if (!userPrefs.categoryPrefs[type]?.app) return false
  2. Heures silencieuses (quietHours)

    typescript
    const now = moment().tz(quietHours.timezone)
    if (now.isBetween(quietHours.start, quietHours.end)) {
      // Ne pas envoyer push/email, seulement socket
    }
  3. Filtrage par workspace actif (subscription.workspaceId)

    typescript
    // Ne push que si l'appareil est dans le bon workspace
    const subscriptions = await prisma.subscription.findMany({
      where: {
        memberId,
        workspaceId: notification.workspaceId,
        isActive: true
      }
    })

Matrice de Communication ​

Appels HTTP REST ​

Service SourceService CibleEndpointFréquence
api-hotelapi-staff-memberPOST /members/add/hotel/memberCréation hôtel
api-hotelapi-staff-memberGET /members/get/by/hotel_id/:idSuppression hôtel
api-hotelapi-stripePOST /sub/assign-subscription/Création hôtel
api-hotelapi-mediaPOST /media/upload/Génération QR
api-authenticationapi-staff-memberGET /members/get/with/:emailChaque login
api-newsapi-staff-memberPOST /members/syncSync périodique
api-notificationapi-staff-memberPOST /members/syncSync périodique

Événements AMQP Émis ​

ServiceRouting KeysDescription
api-authenticationroomee.auth.user.*Création/suppression user
api-hotelroomee.notification.hotel.*CRUD hôtels
api-hotelroomee.storage.mediaConfiguration storage
api-staff-memberroomee.notification.member.*CRUD membres
api-staff-memberroomee.notification.workspace.*CRUD workspaces
api-newsroomee.notification.page.*CRUD pages
api-newsroomee.notification.comment.*CRUD commentaires
api-newsroomee.notification.like.*Likes

Événements AMQP Consommés ​

ServiceRouting KeysAction
api-notificationroomee.notification.*Créer et dispatcher notifications
api-mediaroomee.storage.mediaCréer quota storage

Gestion des Transactions Distribuées ​

Pattern Saga ​

Roomee utilise le pattern Saga pour les opérations multi-services.

Exemple : Création d'écosystème

typescript
try {
  // 1. Créer l'hôtel
  const hotel = await createHotel(data)

  // 2. Générer QR code
  const qrUrl = await generateQR(hotel.id)

  // 3. Créer abonnement Stripe
  const subscription = await createStripeSubscription(hotel.id)

  // 4. Créer administrateur
  const admin = await createAdministrator(hotel.id, adminData)

  // 5. Émettre événement de succès
  await emitEvent('hotel.created', hotel)

} catch (error) {
  // Compensation (rollback)
  if (hotel) await deleteHotel(hotel.id)
  if (subscription) await cancelSubscription(subscription.id)

  await emitEvent('hotel.creation.failed', { error })
  throw error
}

Idempotence ​

Tous les endpoints doivent être idempotents pour supporter les retry :

typescript
// Utiliser des IDs uniques
const notificationId = uuidv4()

await prisma.notification.upsert({
  where: { id: notificationId },
  create: { id: notificationId, ...data },
  update: data
})

Retry et Circuit Breaker ​

AMQP Retry :

typescript
// 3 tentatives avec backoff exponentiel
maxRetries: 3
delays: [1s, 2s, 4s]

HTTP Circuit Breaker :

typescript
const circuitBreaker = new CircuitBreaker(asyncFunction, {
  timeout: 3000,
  errorThresholdPercentage: 50,
  resetTimeout: 10000
})

Bonnes Pratiques ​

1. Toujours Filtrer par Workspace ​

typescript
// ✅ BON
const data = await prisma.page.findMany({
  where: {
    workspaceId: req.user.workspaceId,
    deleted_at: null
  }
})

// ❌ MAUVAIS (fuite multi-tenant)
const data = await prisma.page.findMany()

2. Utiliser les Événements pour les Notifications ​

typescript
// ✅ BON (découplé)
await amqpServer.emit('page.created', pageData)

// ❌ MAUVAIS (couplage fort)
await notificationService.sendNotification(pageData)

3. Vérifier les Permissions ​

typescript
// ✅ BON
if (!hasPermission(user, 'manage_members')) {
  throw new ForbiddenError('Permission denied')
}

// ❌ MAUVAIS (pas de vérification)
await updateMember(data)

4. Logger les Appels Inter-Services ​

typescript
logger.info('Calling api-staff-member', {
  endpoint: '/members/add',
  hotelId,
  timestamp: Date.now()
})

const response = await axios.post(url, data)

logger.info('api-staff-member response', {
  status: response.status,
  duration: Date.now() - timestamp
})

Dépannage ​

Problèmes Courants ​

1. Service indisponible

bash
# Vérifier le service
curl http://localhost:3001/health

# Vérifier les logs
docker logs api-staff-member

# Vérifier AMQP
docker logs rabbitmq

2. Événements AMQP non consommés

bash
# Vérifier les queues
rabbitmqctl list_queues

# Purger une queue
rabbitmqctl purge_queue notification_queue

3. Notifications non reçues

typescript
// Vérifier les préférences
GET /api/v1/preferences

// Vérifier les subscriptions
SELECT * FROM subscription WHERE memberId = '...'

// Vérifier les delivery_attempts
SELECT * FROM delivery_attempt WHERE notificationId = '...'

Monitoring des Interactions ​

Métriques Importantes ​

typescript
// Latence des appels HTTP
histogram('http_request_duration', { service, endpoint })

// Succès/Échec AMQP
counter('amqp_messages_total', { routing_key, status })

// Notifications envoyées
counter('notifications_sent', { channel, status })

Dashboard Grafana ​

Panel 1: Latence inter-services (p50, p95, p99)
Panel 2: Taux d'erreur par service
Panel 3: Throughput AMQP (messages/s)
Panel 4: Taux de livraison notifications

Documentation technique Roomee Services