Skip to main content

WhatsApp Group Booking Announcements

This feature pushes booking lifecycle events (created / modified / cancelled) to WhatsApp groups as a formatted text message — e.g. an owner's group gets a message the moment one of their properties has a booking update.

It lives in admin-api under com.elivaas.pms.groupannounce and reuses the WhatsApp Groups module (WhatsAppGroupService) for the actual send. The booking system (innsync) is the event source; this service only consumes.

How It Works

innsync ──publish booking event──► SNS topic (owner-instant-booking-announcement-*)

├──(A) SQS subscription ──► SQS queue ──► BookingAnnouncementSqsConsumer (@SqsListener)

└──(B) HTTPS subscription ──► POST /api/webhooks/booking-announcement ──► BookingAnnouncementController


GroupAnnouncementService.announce(event)
├─ find active whatsapp_group_subscription rows for (purpose=OWNER_ANNOUNCEMENT, event.propertyIds)
├─ apply the per-subscription event_types filter (NULL = all events)
├─ dedupe to one message per group
├─ skip groups already sent this (event_id, event_type) ── idempotency
└─ WhatsAppGroupService.sendMessage(groupId, text) ── type=text (groups don't accept templates)

Two delivery transports are supported and both feed the same idempotent dispatch, so they are safe to run together:

  • (A) SNS → SQS → consumer — durable buffer, retries, DLQ; survives an admin-api restart/deploy. This is the recommended path for production. Enabled with whatsapp.announcement.sqs.enabled=true.
  • (B) SNS → HTTPS endpoint — no SQS hop; SNS POSTs straight to the app. The controller handles the SubscriptionConfirmation handshake and Notification delivery (envelope or raw). A failed delivery returns 500 so SNS retries.

Idempotency is keyed on (event_id, event_type, group_id) in whatsapp_group_announcement_log. A row is written only after a successful send, so a redelivery (from either transport, or a retry) never double-posts, while a failed send is retried.

Subscriptions (the multi-purpose model)

A WhatsApp group is general-purpose, so it subscribes to a property's events scoped by a purpose. Booking announcements use purpose = OWNER_ANNOUNCEMENT. The same group can hold unrelated subscriptions (other properties, other purposes) without collisions. Adding a new purpose later needs only a new formatter + event source — the group, subscription and sending core are untouched.

Table whatsapp_group_subscription:

ColumnMeaning
group_idMeta group id (FK → whatsapp_group)
purposee.g. OWNER_ANNOUNCEMENT
property_idproperty whose events this group wants
event_typesJSON allow-list e.g. ["BOOKING_CANCELLED"]; NULL = all
activetoggles delivery

Unique on (group_id, purpose, property_id).

Managing subscriptions

POST /api/v1/admin/whatsapp/groups/{groupId}/subscriptions
{ "purpose": "OWNER_ANNOUNCEMENT", "propertyId": "prop_123", "eventTypes": ["BOOKING_CANCELLED"] }

GET /api/v1/admin/whatsapp/groups/{groupId}/subscriptions
DELETE /api/v1/admin/whatsapp/subscriptions/{id}

eventTypes omitted/empty subscribes the group to all booking events for that property.

Event payload

The booking event published by innsync:

{
"title": "Booking Details",
"event": "BOOKING_CANCELLED",
"id": "Ty45",
"property": "CELEST",
"propertyIds": ["prop_123"],
"guest": "Riaz",
"total": 30000,
"paid": 1000,
"checkIn": "2023-12-12",
"checkOut": "2024-12-12",
"guests": 6,
"channel": "MMT",
"status": "CANCELLED",
"mealPlan": "EP - Room Only"
}

event is the type and propertyIds drives routing.

Rendered message

Sent to the group as plain text (type=text). Missing values render as N/A; dates are dd-MM-yy:

Booking Details
🆔 ID: Ty45
🏢 Property: CELEST
👤 Guest: Riaz
💰 Total: 30000
💵 Paid: 1000
📥 Check-in: 12-12-23
📤 Check-out: 12-12-24
👥 Guests: 6
🍽️ Meal Plan: EP - Room Only
🌐 Channel: MMT
📌 Status: CANCELLED

See Deployment for the AWS topic/queue/IAM setup and the env vars that turn this on.