Skip to main content

WhatsApp Group Booking Announcements

This feature pushes booking lifecycle events (created / modified / cancelled) to WhatsApp groups as a pre-approved WhatsApp template 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.sendTemplate(groupId, owner_instant_booking_group_msg) ── sent as an
approved template, then best-effort mirrored into the group's inbox thread

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.

Once a template send succeeds, GroupAnnouncementService also writes it into the group's WhatsApp inbox thread (origin=ANNOUNCEMENT, type=TEMPLATE) so ops sees the announcement alongside whatever replies it provokes. This is best-effort: a failure to record it is logged and swallowed, never surfaced as an announcement failure. Smart Collect nudges (SmartCollectAnnouncementService) do the same. See the WhatsApp Ops Inbox.

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.

Message content

The group receives the pre-approved owner_instant_booking_group_msg template, sent via WhatsAppGroupService.sendTemplate (BookingAnnouncementFormatter.toTemplate) — not a free-form type=text message. (Free-form group text is possible through this codebase — the inbox's own reply path uses sendMessage to send staff replies into a group as plain text — but this feature specifically sends the announcement as an approved template.) Its body carries 11 positional parameters, in this order: ID, Property, Meal Plan, Guest, Total, Paid, Check-in, Check-out, Guests, Channel, Status. A missing value renders as N/A (WhatsApp rejects empty template parameters); dates render as dd-MM-yy. The template's actual wording and layout live in the template registered on the WABA, not in this codebase — the params above are only the values slotted into it.

BookingAnnouncementFormatter.toPlainText renders the same fields as a plain-text summary. This is not what's sent to WhatsApp — it is used only when the announcement is mirrored into the inbox thread (origin=ANNOUNCEMENT, type=TEMPLATE):

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.