erp-transportistas-backend-v2/src/modules/auth/services/roles.service.ts
Adrian Flores Cortes 6e89e7f4c5 feat: Implementar servicios core críticos
- CartaPorteService: CRUD completo para Carta Porte 3.1 SAT
- RolesService: Gestión de roles con aislamiento por tenant
- PermissionsService: Gestión de permisos con bulk creation
- TarifasService: Motor de cotización con múltiples tipos de tarifa
- LanesService: Gestión de rutas origen-destino

Total: ~1,800 líneas de código de servicios

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 10:48:45 -06:00

303 lines
8.0 KiB
TypeScript

import { Repository, FindOptionsWhere, ILike, In } from 'typeorm';
import { Role, Permission, User } from '../entities/index.js';
export interface RoleSearchParams {
tenantId: string;
search?: string;
isSystem?: boolean;
limit?: number;
offset?: number;
}
export interface CreateRoleDto {
name: string;
code: string;
description?: string;
isSystem?: boolean;
color?: string;
permissionIds?: string[];
}
export interface UpdateRoleDto extends Partial<CreateRoleDto> {}
export interface AssignPermissionsDto {
permissionIds: string[];
}
export class RolesService {
constructor(
private readonly roleRepository: Repository<Role>,
private readonly permissionRepository: Repository<Permission>,
) {}
async findAll(params: RoleSearchParams): Promise<{ data: Role[]; total: number }> {
const {
tenantId,
search,
isSystem,
limit = 50,
offset = 0,
} = params;
const where: FindOptionsWhere<Role>[] = [];
const baseWhere: FindOptionsWhere<Role> = { tenantId };
if (isSystem !== undefined) {
baseWhere.isSystem = isSystem;
}
if (search) {
where.push(
{ ...baseWhere, name: ILike(`%${search}%`) },
{ ...baseWhere, code: ILike(`%${search}%`) },
{ ...baseWhere, description: ILike(`%${search}%`) }
);
} else {
where.push(baseWhere);
}
const [data, total] = await this.roleRepository.findAndCount({
where,
relations: ['permissions'],
take: limit,
skip: offset,
order: { isSystem: 'DESC', name: 'ASC' },
});
return { data, total };
}
async findOne(id: string, tenantId: string): Promise<Role | null> {
return this.roleRepository.findOne({
where: { id, tenantId },
relations: ['permissions', 'users'],
});
}
async findByCode(code: string, tenantId: string): Promise<Role | null> {
return this.roleRepository.findOne({
where: { code, tenantId },
relations: ['permissions'],
});
}
async create(tenantId: string, dto: CreateRoleDto, createdBy: string): Promise<Role> {
// Check if code already exists
const existingCode = await this.findByCode(dto.code, tenantId);
if (existingCode) {
throw new Error(`Ya existe un rol con el codigo "${dto.code}"`);
}
const role = this.roleRepository.create({
tenantId,
name: dto.name,
code: dto.code,
description: dto.description ?? null,
isSystem: dto.isSystem ?? false,
color: dto.color ?? null,
createdBy,
});
// If permissions were provided, assign them
if (dto.permissionIds && dto.permissionIds.length > 0) {
const permissions = await this.permissionRepository.findBy({
id: In(dto.permissionIds),
});
role.permissions = permissions;
}
return this.roleRepository.save(role);
}
async update(
id: string,
tenantId: string,
dto: UpdateRoleDto,
updatedBy: string
): Promise<Role | null> {
const role = await this.findOne(id, tenantId);
if (!role) return null;
// Cannot modify system roles
if (role.isSystem && !dto.description && !dto.color) {
throw new Error('Los roles del sistema solo permiten modificar descripcion y color');
}
// Check for code uniqueness if changing code
if (dto.code && dto.code !== role.code) {
const existingCode = await this.findByCode(dto.code, tenantId);
if (existingCode && existingCode.id !== id) {
throw new Error(`Ya existe un rol con el codigo "${dto.code}"`);
}
}
// Update basic fields
if (dto.name !== undefined && !role.isSystem) role.name = dto.name;
if (dto.code !== undefined && !role.isSystem) role.code = dto.code;
if (dto.description !== undefined) role.description = dto.description ?? null;
if (dto.color !== undefined) role.color = dto.color ?? null;
role.updatedBy = updatedBy;
// Update permissions if provided
if (dto.permissionIds !== undefined) {
const permissions = await this.permissionRepository.findBy({
id: In(dto.permissionIds),
});
role.permissions = permissions;
}
return this.roleRepository.save(role);
}
async assignPermissions(
id: string,
tenantId: string,
dto: AssignPermissionsDto,
updatedBy: string
): Promise<Role | null> {
const role = await this.findOne(id, tenantId);
if (!role) return null;
const permissions = await this.permissionRepository.findBy({
id: In(dto.permissionIds),
});
role.permissions = permissions;
role.updatedBy = updatedBy;
return this.roleRepository.save(role);
}
async addPermission(
id: string,
tenantId: string,
permissionId: string,
updatedBy: string
): Promise<Role | null> {
const role = await this.findOne(id, tenantId);
if (!role) return null;
const permission = await this.permissionRepository.findOne({
where: { id: permissionId },
});
if (!permission) {
throw new Error('Permiso no encontrado');
}
// Check if permission already assigned
const hasPermission = role.permissions.some(p => p.id === permissionId);
if (!hasPermission) {
role.permissions.push(permission);
role.updatedBy = updatedBy;
return this.roleRepository.save(role);
}
return role;
}
async removePermission(
id: string,
tenantId: string,
permissionId: string,
updatedBy: string
): Promise<Role | null> {
const role = await this.findOne(id, tenantId);
if (!role) return null;
role.permissions = role.permissions.filter(p => p.id !== permissionId);
role.updatedBy = updatedBy;
return this.roleRepository.save(role);
}
async getPermissions(id: string, tenantId: string): Promise<Permission[]> {
const role = await this.findOne(id, tenantId);
if (!role) return [];
return role.permissions;
}
async getUsers(id: string, tenantId: string): Promise<User[]> {
const role = await this.findOne(id, tenantId);
if (!role) return [];
return role.users;
}
async getUserCount(id: string, tenantId: string): Promise<number> {
const role = await this.roleRepository.findOne({
where: { id, tenantId },
relations: ['users'],
});
return role?.users?.length ?? 0;
}
// Get system roles (admin, user, etc.)
async getSystemRoles(tenantId: string): Promise<Role[]> {
return this.roleRepository.find({
where: { tenantId, isSystem: true },
relations: ['permissions'],
order: { name: 'ASC' },
});
}
// Get custom roles (non-system)
async getCustomRoles(tenantId: string): Promise<Role[]> {
return this.roleRepository.find({
where: { tenantId, isSystem: false },
relations: ['permissions'],
order: { name: 'ASC' },
});
}
// Clone a role
async cloneRole(
id: string,
tenantId: string,
newCode: string,
newName: string,
createdBy: string
): Promise<Role | null> {
const sourceRole = await this.findOne(id, tenantId);
if (!sourceRole) return null;
// Check if new code exists
const existingCode = await this.findByCode(newCode, tenantId);
if (existingCode) {
throw new Error(`Ya existe un rol con el codigo "${newCode}"`);
}
const newRole = this.roleRepository.create({
tenantId,
name: newName,
code: newCode,
description: sourceRole.description,
isSystem: false, // Cloned roles are never system roles
color: sourceRole.color,
permissions: sourceRole.permissions,
createdBy,
});
return this.roleRepository.save(newRole);
}
async delete(id: string, tenantId: string): Promise<boolean> {
const role = await this.findOne(id, tenantId);
if (!role) return false;
// Cannot delete system roles
if (role.isSystem) {
throw new Error('No se pueden eliminar roles del sistema');
}
// Check if role has users
if (role.users && role.users.length > 0) {
throw new Error('No se puede eliminar un rol que tiene usuarios asignados');
}
const result = await this.roleRepository.softDelete(id);
return (result.affected ?? 0) > 0;
}
}