- Introduced GroupWaitlistEntry model in Prisma schema to manage waitlisted users for groups. - Implemented API route to add users to the group waitlist with validation. - Enhanced attendee creation logic to handle group capacity and waitlisting scenarios. - Added new problem builders for handling waitlist-related errors. - Updated frontend to support waitlist interactions, including modals for capacity warnings. - Created tests for waitlist functionality, ensuring proper handling of full groups and existing waitlist entries.
122 lines
3.2 KiB
TypeScript
122 lines
3.2 KiB
TypeScript
import type { PrismaClient } from '@generated/prisma/client';
|
|
import type { CreateAttendee, CreateAttendeeResult, ProblemDetails, Result } from '@gruperly/shared';
|
|
import { err, ok } from '@gruperly/shared';
|
|
import {
|
|
alreadyWaitlistedProblem,
|
|
capacityReachedProblem,
|
|
conflictProblem,
|
|
noGroupAccessProblem,
|
|
notFoundResourceProblem,
|
|
} from '@/http/problem-builders';
|
|
import prisma from '@/lib/prisma';
|
|
import { type AttendeeRecord, normalizePhone, toAttendeeDto } from '../../lib/helpers';
|
|
|
|
type CreateAttendeeDeps = {
|
|
db?: Pick<PrismaClient, 'group' | 'attendee' | 'groupWaitlistEntry'>;
|
|
};
|
|
|
|
type CreateAttendeeOptions = {
|
|
allowOverflow?: boolean;
|
|
};
|
|
|
|
export class CreateAttendeeUseCase {
|
|
constructor(private readonly deps: CreateAttendeeDeps = {}) {}
|
|
|
|
async execute(
|
|
groupId: string,
|
|
userId: string,
|
|
payload: CreateAttendee,
|
|
options: CreateAttendeeOptions = {},
|
|
): Promise<Result<CreateAttendeeResult, ProblemDetails>> {
|
|
const db = this.deps.db ?? prisma;
|
|
|
|
const group = await db.group.findUnique({
|
|
where: { id: groupId },
|
|
include: {
|
|
members: { where: { userId } },
|
|
},
|
|
});
|
|
|
|
if (!group) {
|
|
return err(notFoundResourceProblem('Group', groupId));
|
|
}
|
|
|
|
const isOwner = group.createdById === userId;
|
|
const isMember = group.members.length > 0;
|
|
if (!isOwner && !isMember) {
|
|
return err(noGroupAccessProblem());
|
|
}
|
|
|
|
const phone = normalizePhone(payload.phone);
|
|
const existing = await db.attendee.findFirst({
|
|
where: {
|
|
groupId,
|
|
OR: [
|
|
{ phone },
|
|
{ phone: payload.phone.trim() },
|
|
],
|
|
},
|
|
});
|
|
|
|
if (existing) {
|
|
return err(
|
|
conflictProblem({
|
|
detail: 'Ya existe un alumno registrado con este número de teléfono en este grupo.',
|
|
code: 'attendee_already_registered',
|
|
}),
|
|
);
|
|
}
|
|
|
|
const fullName = `${payload.firstName.trim()} ${payload.lastName.trim()}`.trim();
|
|
|
|
if (group.capacity !== null) {
|
|
const currentCount = await db.attendee.count({ where: { groupId } });
|
|
|
|
if (currentCount >= group.capacity) {
|
|
if (isOwner && !options.allowOverflow) {
|
|
return err(capacityReachedProblem(group.capacity));
|
|
}
|
|
|
|
if (!isOwner) {
|
|
const existingWaitlistEntry = await db.groupWaitlistEntry.findFirst({
|
|
where: { groupId, phone },
|
|
});
|
|
|
|
if (existingWaitlistEntry) {
|
|
return err(alreadyWaitlistedProblem());
|
|
}
|
|
|
|
await db.groupWaitlistEntry.create({
|
|
data: {
|
|
groupId,
|
|
fullName,
|
|
phone,
|
|
email: payload.email?.trim() || null,
|
|
notes: payload.notes?.trim() || null,
|
|
},
|
|
});
|
|
|
|
return ok({
|
|
outcome: 'waitlisted',
|
|
message: `${fullName} fue agregado a la lista de espera del grupo.`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const attendee = await db.attendee.create({
|
|
data: {
|
|
groupId,
|
|
fullName,
|
|
phone,
|
|
email: payload.email?.trim() || null,
|
|
notes: payload.notes?.trim() || null,
|
|
},
|
|
});
|
|
|
|
return ok({
|
|
outcome: 'created',
|
|
attendee: toAttendeeDto(attendee as unknown as AttendeeRecord),
|
|
});
|
|
}
|
|
} |