Broadcast
What it is
Lets an event planner send a one-off message (SMS/email/push, any combination) to their event's guests, pre-paid from the event's PartyBank wallet. Lives inside the events module rather than a standalone module — routes are /v1/event/:eventId/broadcast*.
Core concepts
- Pre-charge, not post-charge: the planner's PartyBank wallet is charged before the broadcast is sent, not billed afterward. If the charge fails (insufficient funds), nothing is sent.
- Per-channel pricing from Firebase, not hardcoded —
broadcastConfigin Firebase Realtime Database holds per-guest unit prices forsms/email/push, fetched live on every broadcast so pricing changes take effect immediately with no deploy. - ₦100 minimum charge — a transfer-API constraint, not a business choice: any calculated cost of ₦1-99 is rounded up to ₦100; a calculated cost of exactly ₦0 (e.g. push-only) is charged nothing and skips the transfer entirely.
- Targeting filters (
itemIds,guestGroups) narrow the recipient list before cost is calculated — you're only charged for guests who actually match the filter, not the event's full guest list.
How it works
GET /event/:eventId/broadcast/charges— preview the cost breakdown (per-channel unit price × eligible guest count) before committing to a send. Eligible guests areINVITEDorCONFIRMEDstatus, not soft-deleted.POST /event/:eventId/broadcastwithtitle,message,channels, and optionallyitemIds/guestGroups:- Filters are applied first (AND logic if both
itemIdsandguestGroupsgiven) to determine the actual recipient list. - Cost is calculated on the filtered count, not the full guest list — the response includes a
costSavingsbreakdown (what you would've paid unfiltered vs. what you actually paid). - Wallet balance is checked; insufficient balance returns a 400 with the current balance and exact shortfall rather than attempting and failing the transfer.
- On success, a transfer moves the charge from the event's PartyBank wallet to the message-processing wallet, then the broadcast is handed off for asynchronous delivery via Pub/Sub — the endpoint returns once the transfer + fanout is initiated, not once every message is confirmed delivered.
- Filters are applied first (AND logic if both
GET /event/:eventId/broadcast/history— every broadcast is permanently recorded (no auto-deletion) with delivered/failed counts per channel, cost, and status (pending → processing → completed, orfailed/partial), queryable by status and paginated.
Key business rules & edge cases
- A broadcast's own history record captures the pricing and filters used at send time (
metadata.channelPricing,metadata.filters,metadata.guestCounts) — so changing Firebase pricing later doesn't retroactively change what a past broadcast is shown to have cost. - Combined filters (
itemIds+guestGroups) are AND, not OR — "guests who bought item 132 AND are in the VIP group," not either/or. If a planner reports "fewer guests than expected," this is the first thing to check. - Requires
authMiddleware+isEventPlanner([EventSections.Rsvp])+ (per the filtering-feature doc) encrypted PIN verification on the send endpoint specifically — broadcast is treated as a spend action, gated like other wallet-debiting operations, not just a messaging action.
Related endpoints
GET /v1/event/{eventId}/broadcast/charges, POST /v1/event/{eventId}/broadcast, GET /v1/event/{eventId}/broadcast/history. Not yet in the generated OpenAPI spec.
Related code
src/modules/events/event.routes.ts(broadcast routes),events.controller.ts,events.service.ts—broadcastMessageToGuests,getBroadcastCharges,getBroadcastHistorysrc/shared/services/event-reminder.service.ts—sendEventBroadcast, filter applicationsrc/shared/workers/messaging-workers.ts— async Pub/Sub delivery processing