changemaker.lite/api/src/modules/people/participant-needs.service.ts

130 lines
3.7 KiB
TypeScript

import { prisma } from '../../config/database';
import { AppError } from '../../middleware/error-handler';
import { logger } from '../../utils/logger';
import type { UpsertNeedsInput } from './participant-needs.schemas';
export const participantNeedsService = {
async upsert(data: UpsertNeedsInput, userId?: string, contactId?: string) {
if (!userId && !contactId) {
throw new AppError(400, 'Either userId or contactId is required', 'MISSING_IDENTIFIER');
}
const where = userId
? { userId }
: { contactId: contactId! };
const existing = await prisma.participantNeeds.findUnique({ where });
if (existing) {
return prisma.participantNeeds.update({
where: { id: existing.id },
data,
});
}
return prisma.participantNeeds.create({
data: {
...data,
...(userId ? { userId } : { contactId }),
},
});
},
async findByUserId(userId: string) {
return prisma.participantNeeds.findUnique({ where: { userId } });
},
async findByContactId(contactId: string) {
return prisma.participantNeeds.findUnique({ where: { contactId } });
},
async aggregateForVoters(userIds: string[], contactIds: string[]) {
const records = await prisma.participantNeeds.findMany({
where: {
OR: [
...(userIds.length > 0 ? [{ userId: { in: userIds } }] : []),
...(contactIds.length > 0 ? [{ contactId: { in: contactIds } }] : []),
],
},
});
const summary = {
total: records.length,
accessibility: {
wheelchair: 0,
groundFloor: 0,
hearingLoop: 0,
signLanguage: 0,
other: 0,
},
dietary: {
vegan: 0,
vegetarian: 0,
glutenFree: 0,
halal: 0,
kosher: 0,
nutAllergy: 0,
other: 0,
},
childcare: 0,
transportation: 0,
translation: 0,
languages: {} as Record<string, number>,
};
for (const r of records) {
if (r.needsWheelchair) summary.accessibility.wheelchair++;
if (r.needsGroundFloor) summary.accessibility.groundFloor++;
if (r.needsHearingLoop) summary.accessibility.hearingLoop++;
if (r.needsSignLanguage) summary.accessibility.signLanguage++;
if (r.otherAccessibility) summary.accessibility.other++;
if (r.isVegan) summary.dietary.vegan++;
if (r.isVegetarian) summary.dietary.vegetarian++;
if (r.isGlutenFree) summary.dietary.glutenFree++;
if (r.isHalal) summary.dietary.halal++;
if (r.isKosher) summary.dietary.kosher++;
if (r.hasNutAllergy) summary.dietary.nutAllergy++;
if (r.otherDietary) summary.dietary.other++;
if (r.needsChildcare) summary.childcare++;
if (r.needsTransportation) summary.transportation++;
if (r.needsTranslation) summary.translation++;
if (r.translationLanguage) {
summary.languages[r.translationLanguage] = (summary.languages[r.translationLanguage] || 0) + 1;
}
}
return summary;
},
async deleteById(id: string) {
const existing = await prisma.participantNeeds.findUnique({ where: { id } });
if (!existing) {
throw new AppError(404, 'Participant needs record not found', 'NOT_FOUND');
}
await prisma.participantNeeds.delete({ where: { id } });
},
async purgeExpired() {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 7);
const result = await prisma.participantNeeds.deleteMany({
where: {
updatedAt: { lt: cutoff },
userId: null,
contactId: null,
},
});
if (result.count > 0) {
logger.info(`Purged ${result.count} orphaned participant needs records older than 7 days`);
}
return { purged: result.count };
},
};