Workstream A — CRM & Notifications:
- Add fire-and-forget CRM activity helper (api/src/utils/crm-activity.ts) hooked into
campaign email, canvass visit, donation, and purchase write sites
- Add 5 operational NotificationType enum values (shift_signup_confirmed, shift_reminder,
shift_cancelled, canvass_session_summary, reengagement) via Prisma migration
- Bridge notification email queue to in-app notifications for volunteer-facing events
- Extend TYPE_TO_PREF map and NotificationsPage labels for new types
Workstream B — Quick Wins:
- Extract shared role constants (11 roles) to admin/src/utils/role-constants.ts,
update 4 consuming pages
- Add Ad Analytics sidebar entry in payments submenu
- Gate 6 calendar routes with enableSocialCalendar feature flag
- Add GET /series/:id/count endpoint and fix hardcoded shiftsCount={0} in ShiftsPage
- Add influenceCampaignId to Order model for donation-campaign attribution,
wire through Stripe checkout metadata
Workstream C — Crash-Safe Scheduled Jobs:
- Create BullMQ scheduled-jobs queue with 10 repeatable job types replacing
setInterval blocks in server.ts (dynamic imports, concurrency: 2)
- Keep presenceService (1min) and challengeScoringService (5min) as setInterval
Bunker Admin
56 lines
1.6 KiB
TypeScript
56 lines
1.6 KiB
TypeScript
import { ContactActivityType } from '@prisma/client';
|
|
import { prisma } from '../config/database';
|
|
import { logger } from './logger';
|
|
|
|
interface RecordActivityParams {
|
|
userId?: string;
|
|
email?: string;
|
|
contactId?: string;
|
|
activityType: ContactActivityType;
|
|
title: string;
|
|
description?: string;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Fire-and-forget CRM activity recorder.
|
|
* Resolves a Contact by userId or email, then writes a ContactActivity row.
|
|
* Skips silently if no matching Contact is found (anonymous users).
|
|
*/
|
|
export async function recordCrmActivity(params: RecordActivityParams): Promise<void> {
|
|
try {
|
|
let contactId = params.contactId;
|
|
|
|
if (!contactId) {
|
|
const conditions: Record<string, unknown>[] = [];
|
|
if (params.userId) conditions.push({ userId: params.userId });
|
|
if (params.email) conditions.push({ email: params.email });
|
|
|
|
if (conditions.length === 0) return;
|
|
|
|
const contact = await prisma.contact.findFirst({
|
|
where: {
|
|
mergedIntoId: null,
|
|
OR: conditions,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
if (!contact) return;
|
|
contactId = contact.id;
|
|
}
|
|
|
|
await prisma.contactActivity.create({
|
|
data: {
|
|
contactId,
|
|
type: params.activityType,
|
|
title: params.title,
|
|
description: params.description,
|
|
metadata: params.metadata as unknown as import('@prisma/client').Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
logger.error('Failed to record CRM activity', { error: err instanceof Error ? err.message : String(err), params: { activityType: params.activityType } });
|
|
}
|
|
}
|