Aller au contenu

Events & messaging

Les events inter-bounded-contexts sont les seuls éléments partagés entre modules. Ils transitent exclusivement via NATS JetStream et sont décrits sous forme de schemas Protobuf dans le package @pms/events-proto.

Chaque event est encapsulé dans une Envelope Protobuf versionnée :

Envelope {
eventId : UUID v4 (idempotence consumers)
eventType : string (ex: "reservation.confirmed")
occurredAt : timestamp
tenantId : UUID
payload : bytes (Protobuf sérialisé)
signature : bytes (HMAC-SHA256, clé rotative)
}

La signature HMAC est reproductible (construction déterministe du message à signer) et permet de détecter toute altération en transit. Les PII ne sont jamais dans les events — seules des références opaques (IDs).

Le package @pms/events-proto contient 24 schemas, dont notamment :

  • reservation.* — created, confirmed, cancelled, no_show, checked_in, checked_out, unit_pre_assigned, unit_assignment_released
  • stay.* — checked_in, checked_out, room_moved
  • folio.* — charge_posted, payment_posted, closed
  • payment.* — captured, refunded
  • deposit.* — recognized (ACOMPTE/ARRHES)
  • housekeeping.* — task_completed
  • work_order.* — closed
  • night_audit.* — completed
  • identity.* — session_revoked
  • accounts_receivable.* — overdue
  • traveler_registry.* — stay_checkout_received

L’atomicité entre l’état de l’agrégat, les events publiés et le journal de versions est garantie par l’outbox pattern :

  1. Le use-case sauvegarde l’agrégat + insère les events dans pms.outbox dans la même transaction
  2. Le worker OutboxRelay (processus séparé) lit la table outbox et publie vers NATS JetStream
  3. Après ACK NATS, la ligne outbox est marquée published_at
  4. En cas de panne relay, les events restent dans la table et sont réémis au redémarrage (at-least-once delivery)
[Use-case TX] → pms.outbox INSERT
[OutboxRelay] → NATS JetStream
[Consumers] (idempotents par eventId)

Tous enregistrés dans apps/api/src/composition/consumer-worker.ts. Chaque consumer est idempotent par eventId ou hold unique.

ConsumerSujetBounded context cible
auditpms.*.>audit
révocationidentity.session_revokedidentity
payment→foliopayment.capturedfolio
deposit-recognition ACOMPTEdeposit.recognizedfiscal-compliance
deposit-recognition ARRHESdeposit.recognizedfiscal-compliance
stay→HKstay.checked_inhousekeeping
HK→inventoryhousekeeping.task_completedinventory
WO→inventorywork_order.closedinventory
cashless-checkoutstay.checked_outcashless
debtor→dunningaccounts_receivable.overdueaccounts-receivable
notification emailnotification.requestednotification
notification SMSnotification.sms_requestednotification
distributionreservation.confirmeddistribution
pos-bridge folio-closedfolio.closedpos-bridge
owner-revenue folio-closedfolio.closedowner-revenue
reporting (×3)night_audit.completedreporting
webhook-dispatcherwebhook.outbound_requestedpublic-api
travelerstay.checked_outtraveler-registry

Convention de nommage : <bounded-context>.<event_type> en minuscules, séparateurs points. JetStream Stream PMS_EVENTS capture pms.>.

Les consumers garantissent l’idempotence via :

  • Déduplication par eventId (table pms.processed_events)
  • Advisory locks transaction-level pour les opérations destructives (pas de session locks — PgBouncer pool_mode=transaction)
  • Idempotency-Key HTTP pour les opérations client (interceptor + store Redis)

Les webhooks vers les intégrateurs tiers transitent par le consumer webhook-dispatcher :

  • Signature HMAC sur le payload (secret par endpoint, rotation possible)
  • Anti-SSRF : whitelist d’URLs cibles
  • Retry automatique avec backoff
  • DLQ (Dead Letter Queue) après N échecs
  • delivery_log append-only (NF203 : inaltérable)