changemaker.lite/api/src/modules/social/integration.service.ts
bunker-admin 902adce646 Add Straw Polls feature: quick opinion polling with public landers, MkDocs widgets, and social integration
Full-stack implementation across 7 sprints:
- Backend: 5 Prisma models (StrawPoll, Option, Vote, Comment, Challenge), 4 enums, POLLS_ADMIN role,
  admin CRUD routes, public voting/SSE/widget endpoints, BullMQ auto-close queue, rate limiting
- Admin: StrawPollsPage with inline drawers (campaigns pattern), PollResults bar chart, sidebar under Advocacy
- Public: dedicated poll lander with real-time SSE updates, browse page, anonymous voting with token dedup
- MkDocs: straw-poll-widget.js hydration (inline vote + card link modes), GrapesJS block types
- Social: feed activity (poll_voted), friend badge integration, challenge notifications, notification preferences
- Feature flag: enablePolls toggle in Settings, FeatureGate, Zod schema

Bunker Admin
2026-03-31 10:16:56 -06:00

143 lines
4.4 KiB
TypeScript

import { prisma } from '../../config/database';
import { friendshipService } from './friendship.service';
export const integrationService = {
/** Get friends who have signed up for a specific shift */
async getFriendsAttendingShift(userId: string, shiftId: string) {
const friendIds = await friendshipService.getFriendIds(userId);
if (friendIds.length === 0) return { friends: [], count: 0 };
// Filter out friends who hid their activity
const hiddenIds = await this.getHiddenActivityUserIds(friendIds);
const visibleIds = friendIds.filter((id) => !hiddenIds.has(id));
if (visibleIds.length === 0) return { friends: [], count: 0 };
const signups = await prisma.shiftSignup.findMany({
where: {
shiftId,
userId: { in: visibleIds },
status: 'CONFIRMED',
},
include: {
user: { select: { id: true, name: true, email: true } },
},
});
return {
friends: signups.map((s) => ({
id: s.user!.id,
name: s.user!.name,
email: s.user!.email,
})),
count: signups.length,
};
},
/** Get friends who participated in a specific campaign (sent emails) */
async getFriendsInCampaign(userId: string, campaignId: string) {
const friendIds = await friendshipService.getFriendIds(userId);
if (friendIds.length === 0) return { friends: [], count: 0 };
const hiddenIds = await this.getHiddenActivityUserIds(friendIds);
const visibleIds = friendIds.filter((id) => !hiddenIds.has(id));
if (visibleIds.length === 0) return { friends: [], count: 0 };
// Get distinct users who sent emails for this campaign
const emails = await prisma.campaignEmail.findMany({
where: {
campaignId,
userId: { in: visibleIds },
},
distinct: ['userId'],
include: {
user: { select: { id: true, name: true, email: true } },
},
});
return {
friends: emails
.filter((e) => e.user)
.map((e) => ({
id: e.user!.id,
name: e.user!.name,
email: e.user!.email,
})),
count: emails.filter((e) => e.user).length,
};
},
/** Get friends with active canvass sessions (currently canvassing) */
async getFriendsActiveOnMap(userId: string) {
const friendIds = await friendshipService.getFriendIds(userId);
if (friendIds.length === 0) return { friends: [], count: 0 };
const hiddenIds = await this.getHiddenActivityUserIds(friendIds);
const visibleIds = friendIds.filter((id) => !hiddenIds.has(id));
if (visibleIds.length === 0) return { friends: [], count: 0 };
const sessions = await prisma.canvassSession.findMany({
where: {
userId: { in: visibleIds },
status: 'ACTIVE',
},
include: {
user: { select: { id: true, name: true, email: true } },
cut: { select: { id: true, name: true } },
},
});
return {
friends: sessions.map((s) => ({
id: s.user.id,
name: s.user.name,
email: s.user.email,
cutName: s.cut.name,
cutId: s.cut.id,
startedAt: s.startedAt,
})),
count: sessions.length,
};
},
/** Get friends who voted on a straw poll */
async getFriendsInStrawPoll(userId: string, pollId: string) {
const friendIds = await friendshipService.getFriendIds(userId);
if (friendIds.length === 0) return { friends: [], count: 0 };
const hiddenIds = await this.getHiddenActivityUserIds(friendIds);
const visibleIds = friendIds.filter((id) => !hiddenIds.has(id));
if (visibleIds.length === 0) return { friends: [], count: 0 };
const votes = await prisma.strawPollVote.findMany({
where: {
pollId,
userId: { in: visibleIds },
},
distinct: ['userId'],
include: {
user: { select: { id: true, name: true, email: true } },
},
});
return {
friends: votes
.filter((v) => v.user)
.map((v) => ({
id: v.user!.id,
name: v.user!.name,
email: v.user!.email,
})),
count: votes.filter((v) => v.user).length,
};
},
/** Helper: get user IDs that have showInFriendActivity disabled */
async getHiddenActivityUserIds(userIds: string[]): Promise<Set<string>> {
const hidden = await prisma.privacySettings.findMany({
where: { userId: { in: userIds }, showInFriendActivity: false },
select: { userId: true },
});
return new Set(hidden.map((p) => p.userId));
},
};