2026-08-29 21:06:30 +03:30
|
|
|
import {
|
|
|
|
|
BadRequestException,
|
|
|
|
|
ConflictException,
|
|
|
|
|
ForbiddenException,
|
|
|
|
|
Injectable,
|
|
|
|
|
NotFoundException,
|
|
|
|
|
} from '@nestjs/common';
|
|
|
|
|
import type { InviteUserRequest, OrgUserDto, UserRole } from '@mento/shared';
|
|
|
|
|
import * as bcrypt from 'bcryptjs';
|
2026-09-11 01:09:59 +03:30
|
|
|
import { Prisma } from '@prisma/client';
|
2026-08-29 21:06:30 +03:30
|
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class UsersService {
|
|
|
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
|
|
|
|
|
|
async list(organizationId: string): Promise<OrgUserDto[]> {
|
|
|
|
|
const rows = await this.prisma.user.findMany({
|
|
|
|
|
where: { organizationId },
|
|
|
|
|
orderBy: [{ role: 'asc' }, { fullName: 'asc' }],
|
|
|
|
|
});
|
|
|
|
|
return rows.map((u) => this.toDto(u));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async invite(
|
|
|
|
|
organizationId: string,
|
|
|
|
|
actorRole: UserRole,
|
|
|
|
|
dto: InviteUserRequest,
|
|
|
|
|
): Promise<OrgUserDto> {
|
|
|
|
|
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
|
|
|
|
throw new ForbiddenException('فقط مالک یا مدیر میتواند کاربر اضافه کند');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const email = dto.email.trim().toLowerCase();
|
|
|
|
|
const existing = await this.prisma.user.findFirst({
|
|
|
|
|
where: { organizationId, email },
|
|
|
|
|
});
|
|
|
|
|
if (existing) {
|
|
|
|
|
throw new ConflictException('این ایمیل در سازمان شما وجود دارد');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const global = await this.prisma.user.findFirst({ where: { email } });
|
|
|
|
|
if (global) {
|
|
|
|
|
throw new ConflictException('این ایمیل قبلاً در سیستم ثبت شده است');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const passwordHash = await bcrypt.hash(dto.password, 10);
|
|
|
|
|
const row = await this.prisma.user.create({
|
|
|
|
|
data: {
|
|
|
|
|
organizationId,
|
|
|
|
|
email,
|
|
|
|
|
fullName: dto.fullName.trim(),
|
|
|
|
|
passwordHash,
|
|
|
|
|
role: dto.role,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return this.toDto(row);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-11 01:09:59 +03:30
|
|
|
async updateCommission(
|
|
|
|
|
organizationId: string,
|
|
|
|
|
actorRole: UserRole,
|
|
|
|
|
targetId: string,
|
|
|
|
|
percent: number,
|
|
|
|
|
): Promise<OrgUserDto> {
|
|
|
|
|
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
|
|
|
|
throw new ForbiddenException('فقط مالک یا مدیر میتواند پورسانت را تعیین کند');
|
|
|
|
|
}
|
|
|
|
|
if (percent < 0 || percent > 100) {
|
|
|
|
|
throw new BadRequestException('درصد پورسانت باید بین ۰ تا ۱۰۰ باشد');
|
|
|
|
|
}
|
|
|
|
|
const target = await this.prisma.user.findFirst({
|
|
|
|
|
where: { id: targetId, organizationId },
|
|
|
|
|
});
|
|
|
|
|
if (!target) throw new NotFoundException('کاربر پیدا نشد');
|
|
|
|
|
const row = await this.prisma.user.update({
|
|
|
|
|
where: { id: target.id },
|
|
|
|
|
data: { commissionPercent: new Prisma.Decimal(percent) },
|
|
|
|
|
});
|
|
|
|
|
return this.toDto(row);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
async updateRole(
|
|
|
|
|
organizationId: string,
|
|
|
|
|
actor: { userId: string; role: UserRole },
|
|
|
|
|
targetId: string,
|
|
|
|
|
nextRole: UserRole,
|
|
|
|
|
): Promise<OrgUserDto> {
|
|
|
|
|
if (actor.role !== 'owner' && actor.role !== 'admin') {
|
|
|
|
|
throw new ForbiddenException('فقط مالک یا مدیر میتواند نقش را تغییر دهد');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const target = await this.prisma.user.findFirst({
|
|
|
|
|
where: { id: targetId, organizationId },
|
|
|
|
|
});
|
|
|
|
|
if (!target) throw new NotFoundException('کاربر پیدا نشد');
|
|
|
|
|
|
|
|
|
|
if (actor.role === 'admin') {
|
|
|
|
|
if (target.role === 'owner' || nextRole === 'owner') {
|
|
|
|
|
throw new ForbiddenException('مدیر نمیتواند نقش مالک را تغییر دهد');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (target.role === 'owner' && nextRole !== 'owner') {
|
|
|
|
|
const owners = await this.prisma.user.count({
|
|
|
|
|
where: { organizationId, role: 'owner' },
|
|
|
|
|
});
|
|
|
|
|
if (owners <= 1) {
|
|
|
|
|
throw new BadRequestException(
|
|
|
|
|
'حداقل یک مالک برای سازمان لازم است؛ ابتدا مالک دیگری تعیین کنید',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const row = await this.prisma.user.update({
|
|
|
|
|
where: { id: target.id },
|
|
|
|
|
data: { role: nextRole },
|
|
|
|
|
});
|
|
|
|
|
return this.toDto(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toDto(row: {
|
|
|
|
|
id: string;
|
|
|
|
|
email: string;
|
|
|
|
|
fullName: string;
|
|
|
|
|
role: UserRole;
|
2026-09-11 01:09:59 +03:30
|
|
|
commissionPercent?: { toString(): string } | number;
|
2026-08-29 21:06:30 +03:30
|
|
|
createdAt: Date;
|
|
|
|
|
}): OrgUserDto {
|
|
|
|
|
return {
|
|
|
|
|
id: row.id,
|
|
|
|
|
email: row.email,
|
|
|
|
|
fullName: row.fullName,
|
|
|
|
|
role: row.role,
|
2026-09-11 01:09:59 +03:30
|
|
|
commissionPercent: Number(row.commissionPercent ?? 0),
|
2026-08-29 21:06:30 +03:30
|
|
|
createdAt: row.createdAt.toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|