Extend EventBus: RC notifications, CRM activity, Gancio migration, calendar source types
- Add 7 new RC notification types: campaign published, donations, subscriptions, SMS escalations, user approved, video published, ticketed events - Add CRM activity entries for subscription activated and email bounced - Migrate ticketed-events Gancio sync from inline calls to EventBus listener - Add meeting.created/deleted events from jitsi.routes.ts - Add SHIFT, MEETING, TICKETED_EVENT to CalendarItemSource enum (Prisma migration) - Update calendar-sync listener to use proper source types instead of MANUAL - Total: 45 listener subscriptions across 6 modules, zero inline sync calls remaining Bunker Admin
This commit is contained in:
parent
075a7c8c4a
commit
68434c51a6
@ -83,10 +83,16 @@ Service Handler (shift created, donation completed, etc.)
|
|||||||
- [x] Media video publish/unpublish/view (3 event types)
|
- [x] Media video publish/unpublish/view (3 event types)
|
||||||
- [x] Ticketed event publish/cancel (EventBus publishes alongside existing Gancio calls)
|
- [x] Ticketed event publish/cancel (EventBus publishes alongside existing Gancio calls)
|
||||||
- [x] Impact story publish (social.impact-story.published)
|
- [x] Impact story publish (social.impact-story.published)
|
||||||
- [ ] Meeting create/update/delete (not yet migrated — meetings module needs review)
|
- [x] Meeting create/delete (jitsi.routes.ts — meeting.created, meeting.deleted)
|
||||||
|
|
||||||
|
### Phase 4b: Extended Listeners (2026-03-31)
|
||||||
|
- [x] RC listener: +7 subscriptions (campaign.published, donations, subscriptions, SMS escalation, user.approved, video.published, ticketed-event.published)
|
||||||
|
- [x] CRM listener: +2 subscriptions (subscription activated, email bounced)
|
||||||
|
- [x] RC webhook service: +7 new formatter methods
|
||||||
|
- [x] Prisma migration: SHIFT, MEETING, TICKETED_EVENT added to CalendarItemSource enum
|
||||||
|
- [x] Calendar sync listener: uses proper source types (SHIFT, MEETING, TICKETED_EVENT)
|
||||||
|
|
||||||
### Phase 5: Future
|
### Phase 5: Future
|
||||||
- [ ] Add SHIFT, MEETING, TICKETED_EVENT to CalendarItemSource enum (Prisma migration)
|
|
||||||
- [ ] Migrate remaining Gancio calls (ticketed-events, meeting-planner) to EventBus
|
- [ ] Migrate remaining Gancio calls (ticketed-events, meeting-planner) to EventBus
|
||||||
- [ ] Add engagement scoring listener
|
- [ ] Add engagement scoring listener
|
||||||
- [ ] Add Homepage dashboard data listener
|
- [ ] Add Homepage dashboard data listener
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
-- Add SHIFT, MEETING, TICKETED_EVENT to CalendarItemSource enum
|
||||||
|
ALTER TYPE "CalendarItemSource" ADD VALUE IF NOT EXISTS 'SHIFT';
|
||||||
|
ALTER TYPE "CalendarItemSource" ADD VALUE IF NOT EXISTS 'MEETING';
|
||||||
|
ALTER TYPE "CalendarItemSource" ADD VALUE IF NOT EXISTS 'TICKETED_EVENT';
|
||||||
@ -4968,6 +4968,9 @@ enum CalendarItemSource {
|
|||||||
MANUAL
|
MANUAL
|
||||||
ICS_FEED
|
ICS_FEED
|
||||||
POLL
|
POLL
|
||||||
|
SHIFT
|
||||||
|
MEETING
|
||||||
|
TICKETED_EVENT
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CalendarRecurrenceFrequency {
|
enum CalendarRecurrenceFrequency {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { prisma } from '../../config/database';
|
|||||||
import { siteSettingsService } from '../settings/settings.service';
|
import { siteSettingsService } from '../settings/settings.service';
|
||||||
import { isServiceOnline } from '../../utils/health-check';
|
import { isServiceOnline } from '../../utils/health-check';
|
||||||
import { generateSlug, generateModeratorToken } from './jitsi.utils';
|
import { generateSlug, generateModeratorToken } from './jitsi.utils';
|
||||||
|
import { eventBus } from '../../services/event-bus.service';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -172,6 +173,14 @@ router.post(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventBus.publish('meeting.created', {
|
||||||
|
meetingId: meeting.id,
|
||||||
|
title: meeting.title,
|
||||||
|
scheduledAt: meeting.startTime?.toISOString() ?? new Date().toISOString(),
|
||||||
|
jitsiRoomName: meeting.jitsiRoom,
|
||||||
|
createdByUserId: meeting.createdByUserId,
|
||||||
|
});
|
||||||
|
|
||||||
res.status(201).json(meeting);
|
res.status(201).json(meeting);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Create meeting failed:', err);
|
logger.error('Create meeting failed:', err);
|
||||||
@ -226,6 +235,12 @@ router.delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await prisma.meeting.delete({ where: { id: meetingId } });
|
await prisma.meeting.delete({ where: { id: meetingId } });
|
||||||
|
|
||||||
|
eventBus.publish('meeting.deleted', {
|
||||||
|
meetingId: meeting.id,
|
||||||
|
title: meeting.title,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Delete meeting failed:', err);
|
logger.error('Delete meeting failed:', err);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { prisma } from '../../config/database';
|
import { prisma } from '../../config/database';
|
||||||
import { TicketedEventStatus, TicketedEventVisibility, EventFormat, Prisma } from '@prisma/client';
|
import { TicketedEventStatus, TicketedEventVisibility, EventFormat, Prisma } from '@prisma/client';
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
import { AppError } from '../../middleware/error-handler';
|
import { AppError } from '../../middleware/error-handler';
|
||||||
import { unifiedCalendarService } from '../events/unified-calendar.service';
|
import { unifiedCalendarService } from '../events/unified-calendar.service';
|
||||||
import { siteSettingsService } from '../settings/settings.service';
|
import { siteSettingsService } from '../settings/settings.service';
|
||||||
@ -385,8 +384,7 @@ export const ticketedEventsService = {
|
|||||||
include: { ticketTiers: true },
|
include: { ticketTiers: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gancio sync + calendar cache bust (fire-and-forget)
|
// Calendar cache bust (fire-and-forget)
|
||||||
this.syncToGancio(updated).catch(() => {});
|
|
||||||
unifiedCalendarService.bustCache().catch(() => {});
|
unifiedCalendarService.bustCache().catch(() => {});
|
||||||
|
|
||||||
eventBus.publish('ticketed-event.published', {
|
eventBus.publish('ticketed-event.published', {
|
||||||
@ -415,8 +413,18 @@ export const ticketedEventsService = {
|
|||||||
include: { ticketTiers: true },
|
include: { ticketTiers: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.syncToGancio(updated).catch(() => {});
|
|
||||||
unifiedCalendarService.bustCache().catch(() => {});
|
unifiedCalendarService.bustCache().catch(() => {});
|
||||||
|
|
||||||
|
eventBus.publish('ticketed-event.published', {
|
||||||
|
eventId: updated.id,
|
||||||
|
title: updated.title,
|
||||||
|
date: updated.date.toISOString().split('T')[0],
|
||||||
|
startTime: updated.startTime,
|
||||||
|
endTime: updated.endTime,
|
||||||
|
location: updated.venueAddress || updated.venueName || undefined,
|
||||||
|
gancioEventId: updated.gancioEventId ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -456,10 +464,7 @@ export const ticketedEventsService = {
|
|||||||
data: { status: 'CANCELLED' },
|
data: { status: 'CANCELLED' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete from Gancio if synced + bust calendar cache
|
// Calendar cache bust (fire-and-forget)
|
||||||
if (event.gancioEventId) {
|
|
||||||
this.deleteFromGancio(event.gancioEventId).catch(() => {});
|
|
||||||
}
|
|
||||||
unifiedCalendarService.bustCache().catch(() => {});
|
unifiedCalendarService.bustCache().catch(() => {});
|
||||||
|
|
||||||
eventBus.publish('ticketed-event.cancelled', {
|
eventBus.publish('ticketed-event.cancelled', {
|
||||||
@ -501,7 +506,10 @@ export const ticketedEventsService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.gancioEventId) {
|
if (event.gancioEventId) {
|
||||||
this.deleteFromGancio(event.gancioEventId).catch(() => {});
|
eventBus.publish('ticketed-event.cancelled', {
|
||||||
|
eventId: event.id,
|
||||||
|
title: event.title,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.ticketedEvent.delete({ where: { id } });
|
await prisma.ticketedEvent.delete({ where: { id } });
|
||||||
@ -783,78 +791,4 @@ export const ticketedEventsService = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- Gancio Sync ---
|
|
||||||
|
|
||||||
async syncToGancio(event: {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description?: string | null;
|
|
||||||
venueAddress?: string | null;
|
|
||||||
venueName?: string | null;
|
|
||||||
eventFormat?: EventFormat;
|
|
||||||
date: Date;
|
|
||||||
startTime: string;
|
|
||||||
endTime: string;
|
|
||||||
gancioEventId?: number | null;
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
const { gancioClient } = await import('../../services/gancio.client');
|
|
||||||
if (!gancioClient.enabled) return;
|
|
||||||
|
|
||||||
// Determine location based on event format
|
|
||||||
let location: string | null;
|
|
||||||
const format = event.eventFormat || 'IN_PERSON';
|
|
||||||
if (format === 'ONLINE') {
|
|
||||||
location = 'Online Event';
|
|
||||||
} else if (format === 'HYBRID') {
|
|
||||||
const venue = event.venueAddress || event.venueName || '';
|
|
||||||
location = venue ? `${venue} (also streaming online)` : 'Online + In-Person';
|
|
||||||
} else {
|
|
||||||
location = event.venueAddress || event.venueName || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = ['ticketed', 'community'];
|
|
||||||
if (format === 'ONLINE') tags.push('online');
|
|
||||||
if (format === 'HYBRID') tags.push('hybrid');
|
|
||||||
|
|
||||||
if (event.gancioEventId) {
|
|
||||||
await gancioClient.updateEvent(event.gancioEventId, {
|
|
||||||
title: event.title,
|
|
||||||
description: event.description,
|
|
||||||
location,
|
|
||||||
date: event.date,
|
|
||||||
startTime: event.startTime,
|
|
||||||
endTime: event.endTime,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const gancioId = await gancioClient.createEvent({
|
|
||||||
title: event.title,
|
|
||||||
description: event.description,
|
|
||||||
location,
|
|
||||||
date: event.date,
|
|
||||||
startTime: event.startTime,
|
|
||||||
endTime: event.endTime,
|
|
||||||
tags,
|
|
||||||
});
|
|
||||||
if (gancioId) {
|
|
||||||
await prisma.ticketedEvent.update({
|
|
||||||
where: { id: event.id },
|
|
||||||
data: { gancioEventId: gancioId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn('Gancio sync failed for ticketed event:', err instanceof Error ? err.message : err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteFromGancio(gancioEventId: number) {
|
|
||||||
try {
|
|
||||||
const { gancioClient } = await import('../../services/gancio.client');
|
|
||||||
if (!gancioClient.enabled) return;
|
|
||||||
await gancioClient.deleteEvent(gancioEventId);
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(`Gancio delete failed for event ${gancioEventId}:`, err instanceof Error ? err.message : err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -156,7 +156,7 @@ async function deleteBySource(sourceType: string, sourceId: string): Promise<voi
|
|||||||
export function registerCalendarSyncListener(): void {
|
export function registerCalendarSyncListener(): void {
|
||||||
// Shift created → Calendar item
|
// Shift created → Calendar item
|
||||||
eventBus.subscribe('shift.created', 'calendar:shift-created', async (payload) => {
|
eventBus.subscribe('shift.created', 'calendar:shift-created', async (payload) => {
|
||||||
await upsertCalendarItem(payload.createdByUserId, 'MANUAL', payload.shiftId, {
|
await upsertCalendarItem(payload.createdByUserId, 'SHIFT', payload.shiftId, {
|
||||||
title: `Shift: ${payload.title}`,
|
title: `Shift: ${payload.title}`,
|
||||||
date: payload.date,
|
date: payload.date,
|
||||||
startTime: payload.startTime,
|
startTime: payload.startTime,
|
||||||
@ -175,7 +175,7 @@ export function registerCalendarSyncListener(): void {
|
|||||||
select: { userId: true },
|
select: { userId: true },
|
||||||
});
|
});
|
||||||
if (!existing) return;
|
if (!existing) return;
|
||||||
await upsertCalendarItem(existing.userId, 'MANUAL', payload.shiftId, {
|
await upsertCalendarItem(existing.userId, 'SHIFT', payload.shiftId, {
|
||||||
title: `Shift: ${payload.title}`,
|
title: `Shift: ${payload.title}`,
|
||||||
date: payload.date,
|
date: payload.date,
|
||||||
startTime: payload.startTime,
|
startTime: payload.startTime,
|
||||||
@ -189,7 +189,7 @@ export function registerCalendarSyncListener(): void {
|
|||||||
|
|
||||||
// Shift deleted → Remove calendar item
|
// Shift deleted → Remove calendar item
|
||||||
eventBus.subscribe('shift.deleted', 'calendar:shift-deleted', async (payload) => {
|
eventBus.subscribe('shift.deleted', 'calendar:shift-deleted', async (payload) => {
|
||||||
await deleteBySource('MANUAL', payload.shiftId);
|
await deleteBySource('SHIFT', payload.shiftId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Meeting created → Calendar item
|
// Meeting created → Calendar item
|
||||||
@ -199,7 +199,7 @@ export function registerCalendarSyncListener(): void {
|
|||||||
const endHour = parseInt(time.split(':')[0]) + 1;
|
const endHour = parseInt(time.split(':')[0]) + 1;
|
||||||
const endTime = `${String(endHour).padStart(2, '0')}:${time.split(':')[1]}`;
|
const endTime = `${String(endHour).padStart(2, '0')}:${time.split(':')[1]}`;
|
||||||
|
|
||||||
await upsertCalendarItem(payload.createdByUserId, 'MANUAL', payload.meetingId, {
|
await upsertCalendarItem(payload.createdByUserId, 'MEETING', payload.meetingId, {
|
||||||
title: `Meeting: ${payload.title}`,
|
title: `Meeting: ${payload.title}`,
|
||||||
date,
|
date,
|
||||||
startTime: time,
|
startTime: time,
|
||||||
@ -223,7 +223,7 @@ export function registerCalendarSyncListener(): void {
|
|||||||
const endHour = parseInt(time.split(':')[0]) + 1;
|
const endHour = parseInt(time.split(':')[0]) + 1;
|
||||||
const endTime = `${String(endHour).padStart(2, '0')}:${time.split(':')[1]}`;
|
const endTime = `${String(endHour).padStart(2, '0')}:${time.split(':')[1]}`;
|
||||||
|
|
||||||
await upsertCalendarItem(existing.userId, 'MANUAL', payload.meetingId, {
|
await upsertCalendarItem(existing.userId, 'MEETING', payload.meetingId, {
|
||||||
title: `Meeting: ${payload.title}`,
|
title: `Meeting: ${payload.title}`,
|
||||||
date,
|
date,
|
||||||
startTime: time,
|
startTime: time,
|
||||||
@ -236,7 +236,7 @@ export function registerCalendarSyncListener(): void {
|
|||||||
|
|
||||||
// Meeting deleted → Remove calendar item
|
// Meeting deleted → Remove calendar item
|
||||||
eventBus.subscribe('meeting.deleted', 'calendar:meeting-deleted', async (payload) => {
|
eventBus.subscribe('meeting.deleted', 'calendar:meeting-deleted', async (payload) => {
|
||||||
await deleteBySource('MANUAL', payload.meetingId);
|
await deleteBySource('MEETING', payload.meetingId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ticketed event published → Calendar item
|
// Ticketed event published → Calendar item
|
||||||
@ -249,7 +249,7 @@ export function registerCalendarSyncListener(): void {
|
|||||||
select: { createdByUserId: true },
|
select: { createdByUserId: true },
|
||||||
});
|
});
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
await upsertCalendarItem(event.createdByUserId, 'MANUAL', payload.eventId, {
|
await upsertCalendarItem(event.createdByUserId, 'TICKETED_EVENT', payload.eventId, {
|
||||||
title: `Event: ${payload.title}`,
|
title: `Event: ${payload.title}`,
|
||||||
date: payload.date,
|
date: payload.date,
|
||||||
startTime: payload.startTime,
|
startTime: payload.startTime,
|
||||||
@ -263,6 +263,6 @@ export function registerCalendarSyncListener(): void {
|
|||||||
|
|
||||||
// Ticketed event cancelled → Remove calendar item
|
// Ticketed event cancelled → Remove calendar item
|
||||||
eventBus.subscribe('ticketed-event.cancelled', 'calendar:ticketed-event-cancel', async (payload) => {
|
eventBus.subscribe('ticketed-event.cancelled', 'calendar:ticketed-event-cancel', async (payload) => {
|
||||||
await deleteBySource('MANUAL', payload.eventId);
|
await deleteBySource('TICKETED_EVENT', payload.eventId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -190,6 +190,27 @@ export function registerCrmActivityListener(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subscription activated
|
||||||
|
eventBus.subscribe('payment.subscription.activated', 'crm:subscription', async (payload) => {
|
||||||
|
const contactId = await findContactByEmail(payload.email);
|
||||||
|
if (!contactId) return;
|
||||||
|
await createActivity(contactId, 'PURCHASE', `Subscribed to "${payload.planName}"`, undefined, {
|
||||||
|
subscriptionId: payload.subscriptionId,
|
||||||
|
planName: payload.planName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listmonk email bounced → flag contact
|
||||||
|
eventBus.subscribe('listmonk.email.bounced', 'crm:email-bounced', async (payload) => {
|
||||||
|
const contactId = await findContactByEmail(payload.subscriberEmail);
|
||||||
|
if (!contactId) return;
|
||||||
|
await createActivity(contactId, 'NOTE_ADDED', `Email bounced (${payload.bounceType})`, `Email address may be invalid — bounced on campaign #${payload.campaignId}`, {
|
||||||
|
listmonkCampaignId: payload.campaignId,
|
||||||
|
bounceType: payload.bounceType,
|
||||||
|
action: 'bounced',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Listmonk email opened → activity
|
// Listmonk email opened → activity
|
||||||
eventBus.subscribe('listmonk.email.opened', 'crm:email-opened', async (payload) => {
|
eventBus.subscribe('listmonk.email.opened', 'crm:email-opened', async (payload) => {
|
||||||
const contactId = await findContactByEmail(payload.subscriberEmail);
|
const contactId = await findContactByEmail(payload.subscriberEmail);
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Gancio EventBus Listener
|
* Gancio EventBus Listener
|
||||||
*
|
*
|
||||||
* Syncs shift events to the Gancio public event calendar.
|
* Syncs shift and ticketed events to the Gancio public event calendar.
|
||||||
* This replaces the inline gancioClient calls in shifts.service.ts.
|
* This replaces the inline gancioClient calls in shifts.service.ts and
|
||||||
|
* ticketed-events.service.ts.
|
||||||
*
|
*
|
||||||
* Feature guard: GANCIO_SYNC_ENABLED=true (checked inside gancioClient)
|
* Feature guard: GANCIO_SYNC_ENABLED=true (checked inside gancioClient)
|
||||||
*/
|
*/
|
||||||
@ -90,4 +91,100 @@ export function registerGancioListener(): void {
|
|||||||
logger.debug('Gancio sync: shift delete failed:', err);
|
logger.debug('Gancio sync: shift delete failed:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// TICKETED EVENT LISTENERS
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Ticketed event published → Create or update Gancio event
|
||||||
|
eventBus.subscribe('ticketed-event.published', 'gancio:ticketed-event-published', async (payload) => {
|
||||||
|
try {
|
||||||
|
const gancio = await getGancioClient();
|
||||||
|
if (!gancio.enabled) return;
|
||||||
|
|
||||||
|
const { prisma } = await import('../../config/database');
|
||||||
|
const event = await prisma.ticketedEvent.findUnique({
|
||||||
|
where: { id: payload.eventId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
venueAddress: true,
|
||||||
|
venueName: true,
|
||||||
|
eventFormat: true,
|
||||||
|
date: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
gancioEventId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!event) return;
|
||||||
|
|
||||||
|
// Determine location based on event format
|
||||||
|
const format = event.eventFormat || 'IN_PERSON';
|
||||||
|
let location: string | null;
|
||||||
|
if (format === 'ONLINE') {
|
||||||
|
location = 'Online Event';
|
||||||
|
} else if (format === 'HYBRID') {
|
||||||
|
const venue = event.venueAddress || event.venueName || '';
|
||||||
|
location = venue ? `${venue} (also streaming online)` : 'Online + In-Person';
|
||||||
|
} else {
|
||||||
|
location = event.venueAddress || event.venueName || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = ['ticketed', 'community'];
|
||||||
|
if (format === 'ONLINE') tags.push('online');
|
||||||
|
if (format === 'HYBRID') tags.push('hybrid');
|
||||||
|
|
||||||
|
if (event.gancioEventId) {
|
||||||
|
// Update existing Gancio event
|
||||||
|
await gancio.updateEvent(event.gancioEventId, {
|
||||||
|
title: event.title,
|
||||||
|
description: event.description,
|
||||||
|
location,
|
||||||
|
date: event.date,
|
||||||
|
startTime: event.startTime,
|
||||||
|
endTime: event.endTime,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new Gancio event and store ID back
|
||||||
|
const gancioId = await gancio.createEvent({
|
||||||
|
title: event.title,
|
||||||
|
description: event.description,
|
||||||
|
location,
|
||||||
|
date: event.date,
|
||||||
|
startTime: event.startTime,
|
||||||
|
endTime: event.endTime,
|
||||||
|
tags,
|
||||||
|
});
|
||||||
|
if (gancioId) {
|
||||||
|
await prisma.ticketedEvent.update({
|
||||||
|
where: { id: event.id },
|
||||||
|
data: { gancioEventId: gancioId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug('Gancio sync: ticketed event publish failed:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ticketed event cancelled → Delete Gancio event
|
||||||
|
eventBus.subscribe('ticketed-event.cancelled', 'gancio:ticketed-event-cancelled', async (payload) => {
|
||||||
|
try {
|
||||||
|
const gancio = await getGancioClient();
|
||||||
|
if (!gancio.enabled) return;
|
||||||
|
|
||||||
|
const { prisma } = await import('../../config/database');
|
||||||
|
const event = await prisma.ticketedEvent.findUnique({
|
||||||
|
where: { id: payload.eventId },
|
||||||
|
select: { gancioEventId: true },
|
||||||
|
});
|
||||||
|
if (!event?.gancioEventId) return;
|
||||||
|
|
||||||
|
await gancio.deleteEvent(event.gancioEventId);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug('Gancio sync: ticketed event cancel failed:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,4 +52,67 @@ export function registerRocketChatListener(): void {
|
|||||||
representativeName: payload.representativeName,
|
representativeName: payload.representativeName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Campaigns ---
|
||||||
|
|
||||||
|
eventBus.subscribe('campaign.published', 'rocketchat:campaign-published', (payload) => {
|
||||||
|
rocketchatWebhookService.onCampaignPublished({
|
||||||
|
campaignTitle: payload.title,
|
||||||
|
campaignSlug: payload.slug,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Payments ---
|
||||||
|
|
||||||
|
eventBus.subscribe('payment.donation.completed', 'rocketchat:donation', (payload) => {
|
||||||
|
rocketchatWebhookService.onDonationReceived({
|
||||||
|
donorName: payload.name || payload.email,
|
||||||
|
amount: (payload.amountCents / 100).toFixed(2),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
eventBus.subscribe('payment.subscription.activated', 'rocketchat:subscription', (payload) => {
|
||||||
|
rocketchatWebhookService.onSubscriptionActivated({
|
||||||
|
userName: payload.name || payload.email,
|
||||||
|
planName: payload.planName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- SMS escalations (QUESTION/NEGATIVE responses) ---
|
||||||
|
|
||||||
|
eventBus.subscribe('sms.message.received', 'rocketchat:sms-escalation', (payload) => {
|
||||||
|
if (payload.responseType === 'QUESTION' || payload.responseType === 'NEGATIVE') {
|
||||||
|
rocketchatWebhookService.onSmsEscalation({
|
||||||
|
phone: payload.phone,
|
||||||
|
responseType: payload.responseType,
|
||||||
|
body: payload.body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Users ---
|
||||||
|
|
||||||
|
eventBus.subscribe('user.approved', 'rocketchat:user-approved', (payload) => {
|
||||||
|
rocketchatWebhookService.onUserApproved({
|
||||||
|
userName: payload.name,
|
||||||
|
role: payload.role,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Media ---
|
||||||
|
|
||||||
|
eventBus.subscribe('media.video.published', 'rocketchat:video-published', (payload) => {
|
||||||
|
rocketchatWebhookService.onVideoPublished({
|
||||||
|
videoTitle: payload.title,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Ticketed Events ---
|
||||||
|
|
||||||
|
eventBus.subscribe('ticketed-event.published', 'rocketchat:ticketed-event', (payload) => {
|
||||||
|
rocketchatWebhookService.onTicketedEventPublished({
|
||||||
|
eventTitle: payload.title,
|
||||||
|
eventDate: payload.date,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,63 @@ class RocketChatWebhookService {
|
|||||||
await this.notify('#campaigns', text, '#9b59b6');
|
await this.notify('#campaigns', text, '#9b59b6');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onCampaignPublished(data: {
|
||||||
|
campaignTitle: string;
|
||||||
|
campaignSlug: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const text = `:newspaper: Campaign published: *${data.campaignTitle}* → /campaigns/${data.campaignSlug}`;
|
||||||
|
await this.notify('#campaigns', text, '#27ae60');
|
||||||
|
}
|
||||||
|
|
||||||
|
async onDonationReceived(data: {
|
||||||
|
donorName: string;
|
||||||
|
amount: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const text = `:money_with_wings: **${data.donorName}** donated **$${data.amount}**`;
|
||||||
|
await this.notify('#campaigns', text, '#f1c40f');
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSubscriptionActivated(data: {
|
||||||
|
userName: string;
|
||||||
|
planName: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const text = `:star: **${data.userName}** subscribed to *${data.planName}*`;
|
||||||
|
await this.notify('#campaigns', text, '#9b59b6');
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSmsEscalation(data: {
|
||||||
|
phone: string;
|
||||||
|
responseType: string;
|
||||||
|
body: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const preview = data.body.length > 100 ? data.body.slice(0, 100) + '...' : data.body;
|
||||||
|
const text = `:warning: SMS ${data.responseType} from ${data.phone}: "${preview}"`;
|
||||||
|
await this.notify('#campaigns', text, '#e74c3c');
|
||||||
|
}
|
||||||
|
|
||||||
|
async onUserApproved(data: {
|
||||||
|
userName: string;
|
||||||
|
role: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const text = `:white_check_mark: User approved: **${data.userName}** (${data.role})`;
|
||||||
|
await this.notify('#campaigns', text, '#2ecc71');
|
||||||
|
}
|
||||||
|
|
||||||
|
async onVideoPublished(data: {
|
||||||
|
videoTitle: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const text = `:film_projector: New video published: *${data.videoTitle}*`;
|
||||||
|
await this.notify('#campaigns', text, '#3498db');
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTicketedEventPublished(data: {
|
||||||
|
eventTitle: string;
|
||||||
|
eventDate: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const text = `:ticket: Event published: *${data.eventTitle}* (${data.eventDate})`;
|
||||||
|
await this.notify('#campaigns', text, '#e67e22');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure default notification channels exist in Rocket.Chat.
|
* Ensure default notification channels exist in Rocket.Chat.
|
||||||
* Called during service startup.
|
* Called during service startup.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user