import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../../prisma/prisma.service'; import { DraftStatus, DraftType } from '@prisma/client'; import { AuditService } from '../audit/audit.service'; import { AuditAction } from '../audit/audit.types'; export interface ApproveDraftResult { publishedId: string; type: DraftType; } @Injectable() export class DraftsService { constructor( private readonly prisma: PrismaService, private readonly audit: AuditService, ) {} list(tenantId: string, type?: DraftType, status: DraftStatus = 'PENDING_REVIEW') { return this.prisma.contentDraft.findMany({ where: { tenantId, ...(type ? { type } : {}), status }, include: { sourceMessage: { select: { id: true, content: true, senderName: true, createdAt: true }, }, }, orderBy: { createdAt: 'desc' }, take: 100, }); } async approve(draftId: string, tenantId: string, adminId: string): Promise { const draft = await this.prisma.contentDraft.findUnique({ where: { id: draftId } }); if (!draft || draft.tenantId !== tenantId) throw new NotFoundException('Draft not found'); if (draft.status !== 'PENDING_REVIEW') { throw new BadRequestException('Draft has already been reviewed'); } const proposed = draft.proposedJson as Record; let publishedId: string; if (draft.type === 'EVENT') { const startsAt = proposed.date ? new Date(`${proposed.date}T${proposed.time ?? '00:00'}:00`) : new Date(); const event = await this.prisma.event.create({ data: { tenantId, title: proposed.title ?? 'Untitled Event', description: proposed.description ?? null, location: proposed.location ?? null, startsAt, createdBy: adminId, isPublished: true, }, }); publishedId = event.id; } else if (draft.type === 'SEVA_TASK') { const startsAt = proposed.date ? new Date(`${proposed.date}T00:00:00`) : undefined; const seva = await this.prisma.sevaOpportunity.create({ data: { tenantId, title: proposed.title ?? 'Seva Opportunity', description: proposed.parentEventTitle ? `Part of: ${proposed.parentEventTitle}` : (proposed.description ?? null), slots: proposed.slots ?? null, startsAt: startsAt ?? null, createdBy: adminId, isPublished: true, }, }); publishedId = seva.id; } else { // FAQ_ITEM — find or create a default board let board = await this.prisma.knowledgeBoard.findFirst({ where: { tenantId }, orderBy: { sortOrder: 'asc' }, }); if (!board) { board = await this.prisma.knowledgeBoard.create({ data: { tenantId, name: 'Community FAQs', slug: 'community-faqs', }, }); } const item = await this.prisma.knowledgeItem.create({ data: { boardId: board.id, tenantId, question: proposed.question ?? 'Untitled question', answer: proposed.answer ?? '', tags: proposed.tags ?? [], status: 'PUBLISHED', createdBy: adminId, }, }); publishedId = item.id; } await this.prisma.contentDraft.update({ where: { id: draftId }, data: { status: 'APPROVED', reviewedBy: adminId, reviewedAt: new Date(), publishedId, }, }); await this.audit.log({ tenantId, actorType: 'ADMIN', actorId: adminId, action: AuditAction.DRAFT_APPROVED, resourceType: 'ContentDraft', resourceId: draftId, payload: { type: draft.type, publishedId }, }); return { publishedId, type: draft.type }; } async reject(draftId: string, tenantId: string, adminId: string): Promise { const draft = await this.prisma.contentDraft.findUnique({ where: { id: draftId } }); if (!draft || draft.tenantId !== tenantId) throw new NotFoundException('Draft not found'); if (draft.status !== 'PENDING_REVIEW') { throw new BadRequestException('Draft has already been reviewed'); } await this.prisma.contentDraft.update({ where: { id: draftId }, data: { status: 'REJECTED', reviewedBy: adminId, reviewedAt: new Date() }, }); await this.audit.log({ tenantId, actorType: 'ADMIN', actorId: adminId, action: AuditAction.DRAFT_REJECTED, resourceType: 'ContentDraft', resourceId: draftId, payload: { type: draft.type }, }); } }