import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { randomBytes } from "crypto"; import { PrismaService } from "../../prisma/prisma.service"; import { AuditService } from "../audit/audit.service"; import { QueuesService } from "../queues/queues.service"; import { CreateInviteeDto, PublicRsvpDto, UpdateInviteeDto } from "./dto"; @Injectable() export class InviteesService { constructor( private readonly prisma: PrismaService, private readonly audit: AuditService, private readonly queues: QueuesService ) {} async findAll(tenantId: string) { return this.prisma.invitee.findMany({ where: { tenantId }, orderBy: { createdAt: "desc" }, include: { event: true } }); } async create(tenantId: string, dto: CreateInviteeDto) { const email = dto.email.toLowerCase().trim(); const event = await this.prisma.event.findFirst({ where: { id: dto.eventId, tenantId } }); if (!event) throw new NotFoundException("Event not found"); const code = randomBytes(10).toString("hex").toUpperCase(); try { const invitee = await this.prisma.invitee.create({ data: { tenantId, eventId: dto.eventId, fullName: dto.fullName, email, phone: dto.phone?.trim().length ? dto.phone : null, code } }); const baseUrl = (process.env.PUBLIC_WEB_URL ?? "http://localhost:3000").replace(/\/$/, ""); const subject = `Invitation: ${event.name}`; const text = `Hello ${invitee.fullName},\n\nYou are invited to ${event.name}.\nRSVP here: ${baseUrl}/invite/${invitee.code}\n\nThank you.`; await this.queues.queue("communications").add("sendEmail", { tenantId, to: invitee.email, subject, text, inviteeId: invitee.id }); await this.audit.log("invitees.create", undefined, { tenantId, entityType: "Invitee", entityId: invitee.id, metadata: { eventId: dto.eventId } }); return invitee; } catch (e: any) { if (typeof e?.message === "string" && e.message.includes("Invitee_tenantId_eventId_email_key")) { throw new BadRequestException("Invitee already exists for this event"); } throw e; } } async findOne(tenantId: string, id: string) { const invitee = await this.prisma.invitee.findFirst({ where: { id, tenantId }, include: { event: true, rsvps: { orderBy: { createdAt: "desc" } } } }); if (!invitee) throw new NotFoundException("Invitee not found"); return invitee; } async update(tenantId: string, id: string, dto: UpdateInviteeDto) { await this.findOne(tenantId, id); const invitee = await this.prisma.invitee.update({ where: { id }, data: { fullName: dto.fullName, email: dto.email ? dto.email.toLowerCase().trim() : undefined, phone: dto.phone === undefined ? undefined : dto.phone?.trim().length ? dto.phone : null, status: dto.status } }); await this.audit.log("invitees.update", undefined, { tenantId, entityType: "Invitee", entityId: id }); return invitee; } async remove(tenantId: string, id: string) { await this.findOne(tenantId, id); await this.prisma.invitee.delete({ where: { id } }); await this.audit.log("invitees.delete", undefined, { tenantId, entityType: "Invitee", entityId: id }); return { ok: true }; } async publicGetInviteeByCode(code: string) { const invitee = await this.prisma.invitee.findFirst({ where: { code }, include: { event: true } }); if (!invitee) throw new NotFoundException("Invite not found"); return { invitee: { code: invitee.code, fullName: invitee.fullName, email: invitee.email, phone: invitee.phone, status: invitee.status }, event: { id: invitee.event.id, name: invitee.event.name, slug: invitee.event.slug, startsAt: invitee.event.startsAt, venue: invitee.event.venue, status: invitee.event.status } }; } async publicRsvp(code: string, dto: PublicRsvpDto) { const invitee = await this.prisma.invitee.findFirst({ where: { code } }); if (!invitee) throw new NotFoundException("Invite not found"); const rsvp = await this.prisma.rSVP.create({ data: { tenantId: invitee.tenantId, eventId: invitee.eventId, inviteeId: invitee.id, response: dto.response, note: dto.note?.trim().length ? dto.note : null } }); await this.prisma.invitee.update({ where: { id: invitee.id }, data: { status: "rsvped" } }); await this.audit.log("rsvps.public", undefined, { tenantId: invitee.tenantId, entityType: "RSVP", entityId: rsvp.id, metadata: { inviteeId: invitee.id, response: dto.response } }); return { ok: true }; } }