import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; import { PrismaService } from '../common/prisma/prisma.service'; @Injectable() export class GroupsService { constructor(private prisma: PrismaService) {} async createGroup(courseId: string, userId: string, name: string, description?: string): Promise { const course = await this.prisma.course.findFirst({ where: { id: courseId, authorId: userId } }); if (!course) throw new ForbiddenException('Only course author can create groups'); return this.prisma.courseGroup.create({ data: { courseId, name, description }, }); } async ensureDefaultGroup(courseId: string): Promise { const existing = await this.prisma.courseGroup.findFirst({ where: { courseId, isDefault: true }, }); if (existing) return existing; return this.prisma.courseGroup.create({ data: { courseId, name: 'Основная группа', description: 'Обсуждение курса и вопросы преподавателю', isDefault: true, }, }); } async getDefaultGroup(courseId: string, userId: string): Promise { const group = await this.ensureDefaultGroup(courseId); await this.assertCanReadGroup(group.id, userId); const [messages, members] = await Promise.all([ this.getGroupMessages(group.id, userId), this.getGroupMembers(group.id, userId), ]); return { group, messages, members }; } async addMember(groupId: string, requesterId: string, targetUserId: string, role = 'student'): Promise { const group = await this.prisma.courseGroup.findUnique({ where: { id: groupId }, include: { course: { select: { authorId: true } } }, }); if (!group) throw new NotFoundException('Group not found'); if (group.course.authorId !== requesterId) { throw new ForbiddenException('Only course author can add members manually'); } return this.prisma.groupMember.upsert({ where: { groupId_userId: { groupId, userId: targetUserId } }, create: { groupId, userId: targetUserId, role }, update: { role }, }); } async getGroupMembers(groupId: string, userId: string): Promise { await this.assertCanReadGroup(groupId, userId); return this.prisma.groupMember.findMany({ where: { groupId }, include: { user: { select: { id: true, name: true, email: true, avatarUrl: true }, }, }, orderBy: { joinedAt: 'asc' }, }); } async getGroupMessages(groupId: string, userId: string): Promise { await this.assertCanReadGroup(groupId, userId); return this.prisma.groupMessage.findMany({ where: { groupId }, include: { user: { select: { id: true, name: true, avatarUrl: true } } }, orderBy: { createdAt: 'asc' }, take: 200, }); } async sendMessage(groupId: string, userId: string, content: string): Promise { await this.assertCanReadGroup(groupId, userId); return this.prisma.groupMessage.create({ data: { groupId, userId, content }, include: { user: { select: { id: true, name: true, avatarUrl: true } } }, }); } async createInviteLink(groupId: string, userId: string): Promise { const group = await this.prisma.courseGroup.findUnique({ where: { id: groupId }, include: { course: { select: { authorId: true } } }, }); if (!group) throw new NotFoundException('Group not found'); if (group.course.authorId !== userId) { throw new ForbiddenException('Only course author can create invite links'); } const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3080'; return { groupId, inviteUrl: `${appUrl}/dashboard/groups/${groupId}`, }; } async joinByInvite(groupId: string, userId: string): Promise { const group = await this.prisma.courseGroup.findUnique({ where: { id: groupId }, include: { course: { select: { id: true, authorId: true, price: true }, }, }, }); if (!group) throw new NotFoundException('Group not found'); const hasEnrollment = await this.prisma.enrollment.findUnique({ where: { userId_courseId: { userId, courseId: group.course.id } }, }); const hasPurchase = await this.prisma.purchase.findUnique({ where: { userId_courseId: { userId, courseId: group.course.id } }, }); if (!hasEnrollment && !hasPurchase && group.course.authorId !== userId) { throw new ForbiddenException('Enroll or purchase course first'); } return this.prisma.groupMember.upsert({ where: { groupId_userId: { groupId, userId } }, create: { groupId, userId, role: group.course.authorId === userId ? 'teacher' : 'student' }, update: {}, }); } async isMember(groupId: string, userId: string): Promise { const member = await this.prisma.groupMember.findUnique({ where: { groupId_userId: { groupId, userId } }, select: { id: true }, }); return Boolean(member); } private async assertCanReadGroup(groupId: string, userId: string): Promise { const group = await this.prisma.courseGroup.findUnique({ where: { id: groupId }, include: { course: { select: { id: true, authorId: true }, }, }, }); if (!group) { throw new NotFoundException('Group not found'); } if (group.course.authorId === userId) return; const member = await this.prisma.groupMember.findUnique({ where: { groupId_userId: { groupId, userId } }, select: { id: true }, }); if (member) return; const enrollment = await this.prisma.enrollment.findUnique({ where: { userId_courseId: { userId, courseId: group.course.id } }, select: { id: true }, }); if (enrollment) { await this.prisma.groupMember.upsert({ where: { groupId_userId: { groupId, userId } }, create: { groupId, userId, role: 'student' }, update: {}, }); return; } throw new ForbiddenException('No access to this group'); } }