import { ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { ContentStatus, MediaEntityType, Prisma } from '@prisma/client'; import { AuthUser } from '../auth/auth.types'; import { PermissionsService } from '../auth/permissions.service'; import { BusinessSettingsService } from '../business-settings/business-settings.service'; import { PrismaService } from '../prisma/prisma.service'; import { TenantService } from '../tenant/tenant.service'; import { CreatePublicCommentDto, ListCommentsDto, ListPublicCommentsDto, UpdateCommentApprovalDto, } from './dto/comment.dto'; type CommentRecord = Prisma.CommentGetPayload<{ include: { approver: true }; }>; @Injectable() export class CommentsService { constructor( private readonly prisma: PrismaService, private readonly permissions: PermissionsService, private readonly tenant: TenantService, private readonly businessSettings: BusinessSettingsService, ) {} async createPublic(host: string, dto: CreatePublicCommentDto) { const business = await this.tenant.resolveBusinessByDomain(host); const businessId = business.id; const entityId = BigInt(dto.entityId); await this.assertPublishedEntityExists(businessId, dto.entityType, entityId); const autoApprove = await this.businessSettings.isCommentsAutoApprove(businessId); const approvedAt = autoApprove ? new Date() : null; const created = await this.prisma.comment.create({ data: { businessId, entityType: dto.entityType, entityId, authorName: dto.authorName.trim(), authorEmail: dto.authorEmail?.trim() || null, text: dto.text.trim(), isApproved: autoApprove, approvedAt, }, include: { approver: true }, }); return { comment: this.serialize(created), message: autoApprove ? 'Comment submitted and is approved' : 'Comment submitted and is pending approval', }; } async listPublic(host: string, query: ListPublicCommentsDto) { const business = await this.tenant.resolveBusinessByDomain(host); const businessId = business.id; const entityId = BigInt(query.entityId); await this.assertPublishedEntityExists(businessId, query.entityType, entityId); const items = await this.prisma.comment.findMany({ where: { businessId, entityType: query.entityType, entityId, isApproved: true, }, orderBy: { createdAt: 'desc' }, include: { approver: true }, }); return { items: items.map((item) => this.serialize(item)) }; } async listAdmin(businessIdRaw: string, query: ListCommentsDto, actor: AuthUser) { const businessId = BigInt(businessIdRaw); await this.assertPermission(businessId, actor.id, 'comments.read'); const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const skip = (page - 1) * pageSize; const where: Prisma.CommentWhereInput = { businessId, ...(query.entityType ? { entityType: query.entityType } : {}), ...(query.entityId ? { entityId: BigInt(query.entityId) } : {}), ...(query.isApproved !== undefined ? { isApproved: query.isApproved } : {}), }; const [items, total] = await Promise.all([ this.prisma.comment.findMany({ where, orderBy: { createdAt: 'desc' }, skip, take: pageSize, include: { approver: true }, }), this.prisma.comment.count({ where }), ]); return { items: items.map((item) => this.serialize(item)), total, page, pageSize, }; } async updateApproval( businessIdRaw: string, commentIdRaw: string, dto: UpdateCommentApprovalDto, actor: AuthUser, ) { const businessId = BigInt(businessIdRaw); const commentId = BigInt(commentIdRaw); await this.assertPermission(businessId, actor.id, 'comments.approve'); const existing = await this.prisma.comment.findFirst({ where: { id: commentId, businessId }, include: { approver: true }, }); if (!existing) { throw new NotFoundException('Comment not found'); } const updated = await this.prisma.comment.update({ where: { id: commentId }, data: { isApproved: dto.isApproved, approvedAt: dto.isApproved ? new Date() : null, approvedBy: dto.isApproved ? actor.id : null, }, include: { approver: true }, }); return { comment: this.serialize(updated) }; } async remove(businessIdRaw: string, commentIdRaw: string, actor: AuthUser) { const businessId = BigInt(businessIdRaw); const commentId = BigInt(commentIdRaw); await this.assertPermission(businessId, actor.id, 'comments.delete'); const existing = await this.prisma.comment.findFirst({ where: { id: commentId, businessId }, }); if (!existing) { throw new NotFoundException('Comment not found'); } await this.prisma.comment.delete({ where: { id: commentId } }); return { success: true }; } private async assertPublishedEntityExists( businessId: bigint, entityType: MediaEntityType, entityId: bigint, ) { if (entityType === MediaEntityType.product) { const product = await this.prisma.product.findFirst({ where: { id: entityId, businessId, status: ContentStatus.published, }, select: { id: true }, }); if (!product) { throw new NotFoundException('Product not found'); } return; } if (entityType === MediaEntityType.blog) { const blog = await this.prisma.blogs.findFirst({ where: { id: entityId, business_id: businessId, status: ContentStatus.published, }, select: { id: true }, }); if (!blog) { throw new NotFoundException('Blog post not found'); } return; } const rows = await this.prisma.$queryRaw<{ id: bigint }[]>` SELECT id FROM portfolios WHERE id = ${entityId} AND business_id = ${businessId} AND status = 'published'::content_status LIMIT 1 `; if (rows.length === 0) { throw new NotFoundException('Portfolio item not found'); } } 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'); } } private serialize(comment: CommentRecord) { return { id: comment.id.toString(), businessId: comment.businessId.toString(), entityType: comment.entityType, entityId: comment.entityId.toString(), authorName: comment.authorName, authorEmail: comment.authorEmail, text: comment.text, isApproved: comment.isApproved, approvedAt: comment.approvedAt, approvedBy: comment.approvedBy?.toString() ?? null, approver: comment.approver ? { id: comment.approver.id.toString(), firstName: comment.approver.firstName, lastName: comment.approver.lastName, } : null, createdAt: comment.createdAt, updatedAt: comment.updatedAt, }; } }