137 lines
4.8 KiB
TypeScript
137 lines
4.8 KiB
TypeScript
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 };
|
|
}
|
|
}
|