Skip to main content

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 — broadcastConfig in Firebase Realtime Database holds per-guest unit prices for sms/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​

  1. GET /event/:eventId/broadcast/charges — preview the cost breakdown (per-channel unit price × eligible guest count) before committing to a send. Eligible guests are INVITED or CONFIRMED status, not soft-deleted.
  2. POST /event/:eventId/broadcast with title, message, channels, and optionally itemIds/guestGroups:
    • Filters are applied first (AND logic if both itemIds and guestGroups given) to determine the actual recipient list.
    • Cost is calculated on the filtered count, not the full guest list — the response includes a costSavings breakdown (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.
  3. 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, or failed/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.

GET /v1/event/{eventId}/broadcast/charges, POST /v1/event/{eventId}/broadcast, GET /v1/event/{eventId}/broadcast/history. Not yet in the generated OpenAPI spec.

  • src/modules/events/event.routes.ts (broadcast routes), events.controller.ts, events.service.ts — broadcastMessageToGuests, getBroadcastCharges, getBroadcastHistory
  • src/shared/services/event-reminder.service.ts — sendEventBroadcast, filter application
  • src/shared/workers/messaging-workers.ts — async Pub/Sub delivery processing