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.
// 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.
// 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.
// 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
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 notificationDétails techniques :
Création de l'hôtel (api-hotel)
typescriptconst hotel = await prisma.hotels.create({ data: { name, email, phone, address, settings: { connect: { id: settingId } }, type: { connect: { id: typeId } } } })Génération QR code (api-media)
typescript// Upload image PNG vers GCP const qrUrl = await uploadQRCode(hotelId, qrImage)Configuration stockage (AMQP → api-media)
typescriptawait amqpServer.emit('roomee.storage.media', { ecosystemId: hotel.id, payload: { max_size: 20000, // 20GB pour Premium unit: 'MO' } })Abonnement Stripe (api-stripe)
typescriptawait axios.post('/sub/assign-subscription/', { customer_id: hotel.id, product_id: 'premium' })Création administrateur (api-staff-member)
typescriptawait 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
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 updateDétails du payload AMQP :
{
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
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 updatePattern de notification graduée :
- Tous les membres du workspace → Socket uniquement (mise à jour UI)
- Auteur de la page → Push + Socket (notification complète)
// 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
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 inscritFlux 5 : Upload de Média
Services impliqués : api-media, api-news
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)
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 :
Préférences utilisateur (
notification_preference)typescriptif (userPrefs.isGlobalSilent) return false if (!userPrefs.categoryPrefs[type]?.app) return falseHeures silencieuses (
quietHours)typescriptconst now = moment().tz(quietHours.timezone) if (now.isBetween(quietHours.start, quietHours.end)) { // Ne pas envoyer push/email, seulement socket }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 Source | Service Cible | Endpoint | Fréquence |
|---|---|---|---|
| api-hotel | api-staff-member | POST /members/add/hotel/member | Création hôtel |
| api-hotel | api-staff-member | GET /members/get/by/hotel_id/:id | Suppression hôtel |
| api-hotel | api-stripe | POST /sub/assign-subscription/ | Création hôtel |
| api-hotel | api-media | POST /media/upload/ | Génération QR |
| api-authentication | api-staff-member | GET /members/get/with/:email | Chaque login |
| api-news | api-staff-member | POST /members/sync | Sync périodique |
| api-notification | api-staff-member | POST /members/sync | Sync périodique |
Événements AMQP Émis
| Service | Routing Keys | Description |
|---|---|---|
| api-authentication | roomee.auth.user.* | Création/suppression user |
| api-hotel | roomee.notification.hotel.* | CRUD hôtels |
| api-hotel | roomee.storage.media | Configuration storage |
| api-staff-member | roomee.notification.member.* | CRUD membres |
| api-staff-member | roomee.notification.workspace.* | CRUD workspaces |
| api-news | roomee.notification.page.* | CRUD pages |
| api-news | roomee.notification.comment.* | CRUD commentaires |
| api-news | roomee.notification.like.* | Likes |
Événements AMQP Consommés
| Service | Routing Keys | Action |
|---|---|---|
| api-notification | roomee.notification.* | Créer et dispatcher notifications |
| api-media | roomee.storage.media | Cré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
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 :
// 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 :
// 3 tentatives avec backoff exponentiel
maxRetries: 3
delays: [1s, 2s, 4s]HTTP Circuit Breaker :
const circuitBreaker = new CircuitBreaker(asyncFunction, {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 10000
})Bonnes Pratiques
1. Toujours Filtrer par Workspace
// ✅ 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
// ✅ BON (découplé)
await amqpServer.emit('page.created', pageData)
// ❌ MAUVAIS (couplage fort)
await notificationService.sendNotification(pageData)3. Vérifier les Permissions
// ✅ 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
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
# Vérifier le service
curl http://localhost:3001/health
# Vérifier les logs
docker logs api-staff-member
# Vérifier AMQP
docker logs rabbitmq2. Événements AMQP non consommés
# Vérifier les queues
rabbitmqctl list_queues
# Purger une queue
rabbitmqctl purge_queue notification_queue3. Notifications non reçues
// 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
// 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