changemaker.lite/api/src/utils/crm-activity.ts
bunker-admin 900a0affe5 Add CRM activity enrichment, notification bridging, crash-safe scheduled jobs, and quick wins
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
2026-03-09 14:15:30 -06:00

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 } });
}
}