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
SubscriptionConfirmationhandshake andNotificationdelivery (envelope or raw). A failed delivery returns500so SNS retries.
Idempotency is keyed on
(event_id, event_type, group_id)inwhatsapp_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:
| Column | Meaning |
|---|---|
group_id | Meta group id (FK → whatsapp_group) |
purpose | e.g. OWNER_ANNOUNCEMENT |
property_id | property whose events this group wants |
event_types | JSON allow-list e.g. ["BOOKING_CANCELLED"]; NULL = all |
active | toggles 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.