v4/Architecture

Webhook Processing Pipeline

This document traces the complete path of a PMS event from HTTP request to device access — every step, every component, and every decision point.


Components involved

ComponentResponsibility
mosler-webhookHTTP ingestion, validation, normalisation, persistence, RabbitMQ publish
RabbitMQMessage queue and dead-letter exchange for retries
mosler-workerBusiness logic, user resolution, booking management, device access provisioning
device-interface-libHardware abstraction — creates eKeys, passcodes, and RFID card access
MongoDBPersists both the raw PMS event (pms_inbound_events) and the resulting booking (bookings_v3)

Phase 1 — Webhook Service (mosler-webhook)

Step 1: Header validation

The request must include X-Company-Id and X-Site-Id. Missing headers return 400 immediately, before any payload parsing.

Step 2: Provider routing

The :provider path segment is validated against the list of supported providers (generic, ezee, hotelogix). An invalid provider returns 400.

Step 3: Payload validation

The provider-specific adapter validates required fields and rejects the request with a 400 if validation fails. For Mosler Direct, required fields are reference_id, action, and at least one date pair. For eZee, required fields are hotel_code, the booking transaction ID, operation, and both dates.

Step 4: Normalisation

The adapter maps provider-specific field names into Mosler's canonical IPmsInboundBookingEvent structure. All field names use snake_case. Action strings are mapped to typed event enum values (BOOKING_CREATE, etc.).

Step 5: Duplicate detection

Before persisting, the service queries MongoDB for any event with the same company_id, reference_id, and event_type received within the last 60 seconds. If a duplicate is found:

  • The incoming event is stored with status = DUPLICATE (for audit trail)
  • The original event's ID and status are returned immediately
  • No new RabbitMQ message is published

This prevents double-processing when a PMS retries a failed HTTP request.

Step 6: Persistence

The normalised event is saved to MongoDB (pms_inbound_events collection) with status = RECEIVED.

Step 7: RabbitMQ publish

The event is published to the pms.booking.events exchange. On success, status is updated to QUEUED and a 202 Accepted is returned to the PMS. On failure, status is set to FAILED and a 400 is returned (so the PMS knows to retry).


Phase 2 — Worker (mosler-worker)

Step 8: Consume from queue

The worker subscribes to the queue bound to pms.booking.events. Each message is acknowledged only after successful processing (or permanently rejected after max retries).

Step 9: Event type dispatch

Based on event_type, the message is routed to the appropriate handler:

Event typeHandler
BOOKING_CREATEexecuteCreate()
BOOKING_UPDATEexecuteUpdate()
BOOKING_CANCELexecuteCancel()
BOOKING_CHECKINexecuteCheckin()
BOOKING_CHECKOUTexecuteCheckout()
BOOKING_ROOMMOVEexecuteRoomMove()
BOOKING_DATECHANGEexecuteDateChange()

Step 10: Idempotency check

For create events, the worker first queries bookings_v3 by company_id + reference_id. If a booking already exists, it skips processing and publishes a status update (COMPLETED) without creating a duplicate. This guard covers the case where the worker restarts mid-flight and reprocesses an already-completed message.

Step 11: User resolution

The worker runs the user resolution algorithm — parallel phone and email lookups, duplicate disambiguation, contact-info merging, or account creation. See the full algorithm in User Resolution Architecture.

Step 12: Room lookup

The room is found by room_number within the resolved site_id. If no room is found, processing fails and the event transitions to FAILED.

Step 13: Booking creation / update / cancellation

The appropriate booking record is created or mutated in bookings_v3 with the resolved user, room, dates, and access type.

Step 14: Device access provisioning

For create/checkin events, the worker calls GrantBookingAccessUseCase, which uses device-interface-lib to communicate with the lock hardware:

  • EKEY → creates a mobile (Bluetooth) key for the guest via the lock vendor's API
  • PASSCODE → generates a time-limited PIN
  • CARD → programs the RFID card ID onto the lock
  • MULTI → all of the above

The lock vendor is abstracted behind device-interface-lib, so the same provisioning logic works across manufacturers.

For cancel/checkout events, access is revoked.

Step 15: Status update

After successful processing, the worker publishes a domain event that transitions the PMS inbound event record from QUEUEDCOMPLETED.


Retry & failure handling

Processing fails
      │
      ▼
Worker nacks message (false, false) → message goes to DLQ
      │
      ▼
DLQ consumer re-queues with incremented retry_count
      │
      ├── retry_count < 3 → reprocessed
      │
      └── retry_count ≥ 3 → message permanently rejected
                            event status → DEAD_LETTER

Retries use RabbitMQ's x-dead-letter-exchange with a delay. Each retry attempt increments retry_count on both the RabbitMQ message headers and the MongoDB event record.


Observability

SignalWhere
Event status transitionsMongoDB pms_inbound_events.status field
Processing logsWinston → AWS CloudWatch
Event counter by type and outcomePrometheus mosler_worker_events_total{event_type, status}
DLQ depth gaugePrometheus mosler_dlq_messages{queue}
AlertsSlack / Telegram via DlqMonitorService when queue depth exceeds threshold
Distributed tracesOpenTelemetry → Grafana Tempo

All logs include eventId and correlationId for end-to-end trace correlation between the webhook service and the worker.