import { Injectable, NotFoundException, ConflictException, } from '@nestjs/common'; import * as bcrypt from 'bcrypt'; import { PrismaService } from '../prisma/prisma.service'; import { JournalService } from '../accounting/journal.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; @Injectable() export class UserService { constructor( private readonly prisma: PrismaService, private readonly journal: JournalService, ) {} private formatUser(user: any) { return { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, isActive: user.isActive, roles: user.roles?.map((r: any) => r.role) ?? [], tenantRoles: user.tenantRoles?.map((tr: any) => ({ id: tr.tenantRole.id, name: tr.tenantRole.name, slug: tr.tenantRole.slug, })) ?? [], createdAt: user.createdAt, }; } private readonly userInclude = { roles: { select: { role: true } }, tenantRoles: { include: { tenantRole: { select: { id: true, name: true, slug: true } }, }, }, }; async findAll(tenantId: string) { const db = this.prisma.forTenant(tenantId); const users = await db.user.findMany({ include: this.userInclude, orderBy: { createdAt: 'desc' }, }); return users.map((u) => this.formatUser(u)); } async findForMention(tenantId: string) { const db = this.prisma.forTenant(tenantId); const users = await db.user.findMany({ where: { isActive: true }, select: { id: true, firstName: true, lastName: true }, orderBy: { firstName: 'asc' }, }); return users; } async findById(tenantId: string, userId: string) { const db = this.prisma.forTenant(tenantId); const user = await db.user.findFirst({ where: { id: userId }, include: this.userInclude, }); if (!user) throw new NotFoundException('User not found'); return this.formatUser(user); } async create(tenantId: string, dto: CreateUserDto) { const existing = await this.prisma.user.findFirst({ where: { tenantId, email: dto.email }, }); if (existing) throw new ConflictException('Email already exists in this tenant'); const hashedPassword = await bcrypt.hash(dto.password, 12); const user = await this.prisma.user.create({ data: { tenantId, email: dto.email, password: hashedPassword, firstName: dto.firstName, lastName: dto.lastName, }, }); // Assign tenant roles if provided if (dto.tenantRoleIds?.length) { await this.prisma.userTenantRole.createMany({ data: dto.tenantRoleIds.map((roleId) => ({ userId: user.id, tenantRoleId: roleId, })), }); } // Legacy: also create UserRole for backward compat (uses first tenant role slug) if (dto.roles?.length) { await this.prisma.userRole.createMany({ data: dto.roles.map((role) => ({ userId: user.id, role })), }); } // Auto-create custodial CoA accounts this.journal.createCustodialAccounts( tenantId, user.id, `${user.firstName} ${user.lastName}`, ).catch(() => {}); const created = await this.prisma.user.findUnique({ where: { id: user.id }, include: this.userInclude, }); return this.formatUser(created); } async update(tenantId: string, userId: string, dto: UpdateUserDto) { const db = this.prisma.forTenant(tenantId); const existing = await db.user.findFirst({ where: { id: userId } }); if (!existing) throw new NotFoundException('User not found'); // Update basic fields const updateData: any = {}; if (dto.firstName) updateData.firstName = dto.firstName; if (dto.lastName) updateData.lastName = dto.lastName; if (Object.keys(updateData).length > 0) { await db.user.update({ where: { id: userId }, data: updateData, }); } // Update tenant roles if provided if (dto.tenantRoleIds !== undefined) { // Verify all role IDs belong to this tenant const roles = await this.prisma.tenantRole.findMany({ where: { id: { in: dto.tenantRoleIds }, tenantId, deletedAt: null }, }); if (roles.length !== dto.tenantRoleIds.length) { throw new NotFoundException('One or more role IDs are invalid'); } await this.prisma.userTenantRole.deleteMany({ where: { userId } }); if (dto.tenantRoleIds.length > 0) { await this.prisma.userTenantRole.createMany({ data: dto.tenantRoleIds.map((roleId) => ({ userId, tenantRoleId: roleId, })), }); } } // Legacy role update (backward compat) if (dto.roles) { await this.prisma.userRole.deleteMany({ where: { userId } }); await this.prisma.userRole.createMany({ data: dto.roles.map((role) => ({ userId, role })), }); } const updated = await this.prisma.user.findUnique({ where: { id: userId }, include: this.userInclude, }); return this.formatUser(updated); } async toggleActive(tenantId: string, userId: string) { const db = this.prisma.forTenant(tenantId); const existing = await db.user.findFirst({ where: { id: userId } }); if (!existing) throw new NotFoundException('User not found'); const user = await this.prisma.user.update({ where: { id: userId }, data: { isActive: !existing.isActive }, include: this.userInclude, }); return this.formatUser(user); } }