import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { AuthUser } from '../auth/auth.types'; import { PermissionsService } from '../auth/permissions.service'; import { PrismaService } from '../prisma/prisma.service'; import { TenantService } from '../tenant/tenant.service'; import { CreateContactSubmissionDto } from './dto/create-contact-submission.dto'; import { ListContactSubmissionsDto } from './dto/list-contact-submissions.dto'; type ContactSubmissionRow = { id: bigint; title: string; name: string; email: string | null; cellNumber: string | null; text: string; createdAt: Date; }; @Injectable() export class ContactSubmissionsService { constructor( private readonly prisma: PrismaService, private readonly permissions: PermissionsService, private readonly tenant: TenantService, ) {} async createPublic(host: string, dto: CreateContactSubmissionDto) { const business = await this.tenant.resolveBusinessByDomain(host); const created = await this.prisma.contactSubmission.create({ data: { businessId: business.id, title: dto.title.trim(), name: dto.name.trim(), email: dto.email?.trim() || null, cellNumber: dto.cellNumber?.trim() || null, text: dto.text.trim(), }, }); return { submission: this.serialize(created), message: 'Contact form submitted successfully', }; } async list( businessIdRaw: string, query: ListContactSubmissionsDto, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); await this.assertPermission(businessId, actor.id, 'business.read'); const page = query.page ?? 1; const pageSize = Math.min(Math.max(query.pageSize ?? 20, 1), 100); const skip = (page - 1) * pageSize; const qLike = query.q?.trim() ? `%${query.q.trim()}%` : null; const where = Prisma.sql` WHERE cs.business_id = ${businessId} ${qLike ? Prisma.sql` AND ( cs.title ILIKE ${qLike} OR cs.name ILIKE ${qLike} OR cs.email ILIKE ${qLike} OR cs.cell_number ILIKE ${qLike} OR cs.text ILIKE ${qLike} ) ` : Prisma.empty} `; const [items, totalRow] = await Promise.all([ this.prisma.$queryRaw(Prisma.sql` SELECT cs.id AS "id", cs.title AS "title", cs.name AS "name", cs.email AS "email", cs.cell_number AS "cellNumber", cs.text AS "text", cs.created_at AS "createdAt" FROM contact_submissions cs ${where} ORDER BY cs.created_at DESC LIMIT ${pageSize} OFFSET ${skip} `), this.prisma.$queryRaw<{ total: number }[]>(Prisma.sql` SELECT COUNT(*)::int AS "total" FROM contact_submissions cs ${where} `), ]); return { items: items.map((row) => this.serialize(row)), total: totalRow[0]?.total ?? 0, page, pageSize, }; } async getOne(businessIdRaw: string, submissionIdRaw: string, actor: AuthUser) { const businessId = BigInt(businessIdRaw); const submissionId = BigInt(submissionIdRaw); await this.assertPermission(businessId, actor.id, 'business.read'); const submission = await this.prisma.contactSubmission.findFirst({ where: { id: submissionId, businessId }, }); if (!submission) { throw new NotFoundException('Contact submission not found'); } return { submission: this.serialize(submission) }; } private serialize(row: ContactSubmissionRow | { id: bigint; title: string; name: string; email: string | null; cellNumber: string | null; text: string; createdAt: Date; }) { return { id: row.id.toString(), title: row.title, name: row.name, email: row.email, cellNumber: row.cellNumber, text: row.text, createdAt: row.createdAt.toISOString(), }; } private async assertPermission( businessId: bigint, userId: bigint, permission: string, ) { const allowed = await this.permissions.hasBusinessPermission( userId, businessId, permission, ); if (!allowed) { throw new ForbiddenException('Insufficient permissions'); } } }