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