mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 22:30:55 +00:00
- Added detailed logging for socket connection events in useBookingWindowSocket. - Introduced new notification types for contract status and schedule updates. - Updated notification visuals to include new icons for contract status. - Enhanced notification href resolution for contract status and schedule updates. - Implemented booking lifecycle notifier service for customer and staff notifications. - Created contract notifier service for managing contract lifecycle notifications. - Added end-to-end tests for booking window socket functionality.
445 lines
12 KiB
TypeScript
445 lines
12 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from "@nestjs/common";
|
|
import { InjectRepository } from "@nestjs/typeorm";
|
|
import { hashPassword } from "@tria-plc/api-common/utils/argon";
|
|
import { EUserStatus } from "@tria-plc/api-common/utils/enums/user.enum";
|
|
import { DataSource, EntityManager, In, IsNull, Repository } from "typeorm";
|
|
|
|
// Subpath imports (not the package root) so ts-jest can resolve them when this
|
|
// file lands in a spec's compile graph via the notification recipients chain.
|
|
import { Employee } from "@tria-plc/iamapi-common/entities/iam/organization-structure/employee.entity";
|
|
import { Organization } from "@tria-plc/iamapi-common/entities/iam/organization-structure/organization.entity";
|
|
import { UserCredential } from "@tria-plc/iamapi-common/entities/iam/user/user-credential.entity";
|
|
import { Role } from "@tria-plc/iamapi-common/entities/iam/user/role.entity";
|
|
import { UserRole } from "@tria-plc/iamapi-common/entities/iam/user/user-role.entity";
|
|
import { User } from "@tria-plc/iamapi-common/entities/iam/user/user.entity";
|
|
|
|
import { CreateOrganizationUserDto } from "./dto/create-organization-user.dto";
|
|
|
|
const RESERVED_ROLE_KEYS = new Set([
|
|
"super_admin",
|
|
"organization_admin",
|
|
"unit_admin",
|
|
]);
|
|
const DEFAULT_USER_PASSWORD = "12345678";
|
|
const ORGANIZATION_ADMIN_ROLE_KEY = "organization_admin";
|
|
const EDR_ORG_MANAGER_ROLE_KEY = "edr_org_manager";
|
|
|
|
@Injectable()
|
|
export class BackofficeService {
|
|
constructor(
|
|
@InjectRepository(Employee)
|
|
private readonly employeeRepository: Repository<Employee>,
|
|
@InjectRepository(Organization)
|
|
private readonly organizationRepository: Repository<Organization>,
|
|
@InjectRepository(Role)
|
|
private readonly roleRepository: Repository<Role>,
|
|
@InjectRepository(UserRole)
|
|
private readonly userRoleRepository: Repository<UserRole>,
|
|
@InjectRepository(User)
|
|
private readonly userRepository: Repository<User>,
|
|
private readonly dataSource: DataSource,
|
|
) {}
|
|
|
|
/**
|
|
* IAM user ids of every current employee across all organizations — used by
|
|
* the notification recipients resolver's `allBackoffice` selector.
|
|
*/
|
|
async getAllCurrentEmployeeUserIds(): Promise<string[]> {
|
|
const employees = await this.employeeRepository.find({
|
|
where: { isCurrent: true },
|
|
});
|
|
return [
|
|
...new Set(
|
|
employees.map((e) => e.userId).filter((id): id is string => Boolean(id)),
|
|
),
|
|
];
|
|
}
|
|
|
|
async createOrganizationUser(
|
|
organizationId: string,
|
|
dto: CreateOrganizationUserDto,
|
|
) {
|
|
const organizationExists = await this.organizationRepository.exists({
|
|
where: { id: organizationId },
|
|
});
|
|
|
|
if (!organizationExists) {
|
|
throw new NotFoundException("organization_not_found");
|
|
}
|
|
|
|
const email = dto.email.trim().toLowerCase();
|
|
const username = dto.username.trim().toLowerCase();
|
|
const phoneNumber = dto.phoneNumber?.trim() || undefined;
|
|
const assignOrganizationAdmin = dto.assignOrganizationAdmin === true;
|
|
const name = {
|
|
en: dto.name.en.trim(),
|
|
...(dto.name.am?.trim() ? { am: dto.name.am.trim() } : {}),
|
|
};
|
|
|
|
const existingUsers = await this.userRepository.find({
|
|
where: [{ email }, { username }],
|
|
select: { id: true, email: true, username: true },
|
|
});
|
|
|
|
const emailUser = existingUsers.find((user) => user.email === email);
|
|
const usernameUser = existingUsers.find((user) => user.username === username);
|
|
|
|
if (emailUser && usernameUser && emailUser.id !== usernameUser.id) {
|
|
throw new BadRequestException("email_or_username_already_in_use");
|
|
}
|
|
|
|
const existingUser = emailUser ?? usernameUser;
|
|
const hashedPassword = await hashPassword(DEFAULT_USER_PASSWORD);
|
|
|
|
return this.dataSource.transaction(async (manager) => {
|
|
let user = existingUser;
|
|
|
|
if (!user) {
|
|
user = await manager.getRepository(User).save(
|
|
manager.getRepository(User).create({
|
|
email,
|
|
username,
|
|
phoneNumber,
|
|
name,
|
|
isActive: true,
|
|
hasSetPassword: true,
|
|
status: EUserStatus.ACCEPTED,
|
|
}),
|
|
);
|
|
} else {
|
|
await manager.getRepository(User).update(
|
|
{ id: user.id },
|
|
{
|
|
email,
|
|
username,
|
|
phoneNumber,
|
|
name,
|
|
isActive: true,
|
|
hasSetPassword: true,
|
|
status: EUserStatus.ACCEPTED,
|
|
},
|
|
);
|
|
}
|
|
|
|
const activeCredentialExists = await manager.getRepository(UserCredential).exists({
|
|
where: {
|
|
userId: user.id,
|
|
isActive: true,
|
|
},
|
|
});
|
|
|
|
if (!activeCredentialExists) {
|
|
await manager.getRepository(UserCredential).insert({
|
|
userId: user.id,
|
|
password: hashedPassword,
|
|
isActive: true,
|
|
});
|
|
}
|
|
|
|
let employee = await manager.getRepository(Employee).findOne({
|
|
where: {
|
|
userId: user.id,
|
|
organizationId,
|
|
isCurrent: true,
|
|
},
|
|
relations: {
|
|
user: true,
|
|
employeePositions: {
|
|
position: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!employee) {
|
|
const insertResult = await manager.getRepository(Employee).insert({
|
|
userId: user.id,
|
|
organizationId,
|
|
isCurrent: true,
|
|
name,
|
|
});
|
|
|
|
employee = await manager.getRepository(Employee).findOne({
|
|
where: { id: insertResult.identifiers[0]?.id as string },
|
|
relations: {
|
|
user: true,
|
|
employeePositions: {
|
|
position: true,
|
|
},
|
|
},
|
|
});
|
|
} else {
|
|
await manager.getRepository(Employee).update(
|
|
{ id: employee.id },
|
|
{ name },
|
|
);
|
|
|
|
employee = await manager.getRepository(Employee).findOne({
|
|
where: { id: employee.id },
|
|
relations: {
|
|
user: true,
|
|
employeePositions: {
|
|
position: true,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
if (!employee) {
|
|
throw new NotFoundException("employee_create_failed");
|
|
}
|
|
|
|
const userId = user.id;
|
|
|
|
if (!userId) {
|
|
throw new NotFoundException("user_create_failed");
|
|
}
|
|
|
|
if (assignOrganizationAdmin) {
|
|
await this.ensureOrganizationAdminAccess(manager, organizationId, userId);
|
|
}
|
|
|
|
return employee;
|
|
});
|
|
}
|
|
|
|
async getEmployeeUserRoles(organizationId: string, userId: string) {
|
|
await this.assertUserBelongsToOrganization(organizationId, userId);
|
|
|
|
const userRoles = await this.userRoleRepository.find({
|
|
where: {
|
|
userId,
|
|
organizationId,
|
|
unitId: IsNull(),
|
|
},
|
|
relations: {
|
|
role: true,
|
|
},
|
|
order: {
|
|
role: {
|
|
key: "ASC",
|
|
},
|
|
},
|
|
});
|
|
|
|
return userRoles
|
|
.map((userRole) => userRole.role)
|
|
.filter((role): role is Role => Boolean(role))
|
|
.map((role) => ({
|
|
id: role.id,
|
|
key: role.key,
|
|
name: role.name,
|
|
}));
|
|
}
|
|
|
|
async getOrganizationEmployees(
|
|
organizationId: string,
|
|
query: { skip?: string; take?: string },
|
|
) {
|
|
const organizationExists = await this.organizationRepository.exists({
|
|
where: { id: organizationId },
|
|
});
|
|
|
|
if (!organizationExists) {
|
|
throw new NotFoundException("organization_not_found");
|
|
}
|
|
|
|
const take = Number.parseInt(query.take ?? "1000", 10);
|
|
const skip = Number.parseInt(query.skip ?? "0", 10);
|
|
|
|
const employees = await this.employeeRepository.find({
|
|
where: {
|
|
organizationId,
|
|
isCurrent: true,
|
|
},
|
|
relations: {
|
|
user: true,
|
|
employeePositions: {
|
|
position: true,
|
|
},
|
|
},
|
|
order: {
|
|
createdAt: "DESC",
|
|
},
|
|
});
|
|
|
|
const deduplicated = this.mergeEmployeesByUser(employees);
|
|
|
|
return {
|
|
count: deduplicated.length,
|
|
items: deduplicated.slice(skip, skip + take),
|
|
};
|
|
}
|
|
|
|
async replaceEmployeeUserRoles(
|
|
organizationId: string,
|
|
userId: string,
|
|
roleIds: string[],
|
|
) {
|
|
await this.assertUserBelongsToOrganization(organizationId, userId);
|
|
|
|
const uniqueRoleIds = [...new Set(roleIds)];
|
|
const roles = uniqueRoleIds.length
|
|
? await this.roleRepository.find({
|
|
where: {
|
|
id: In(uniqueRoleIds),
|
|
},
|
|
})
|
|
: [];
|
|
|
|
if (roles.length !== uniqueRoleIds.length) {
|
|
throw new NotFoundException("one_or_more_roles_not_found");
|
|
}
|
|
|
|
const reservedRoles = roles.filter((role) => RESERVED_ROLE_KEYS.has(role.key));
|
|
if (reservedRoles.length) {
|
|
throw new BadRequestException("reserved_roles_must_use_admin_actions");
|
|
}
|
|
|
|
await this.dataSource.transaction(async (manager) => {
|
|
await manager.getRepository(UserRole).delete({
|
|
userId,
|
|
organizationId,
|
|
unitId: IsNull(),
|
|
});
|
|
|
|
if (!roles.length) {
|
|
return;
|
|
}
|
|
|
|
await manager.getRepository(UserRole).insert(
|
|
roles.map((role) => ({
|
|
userId,
|
|
roleId: role.id,
|
|
organizationId,
|
|
})),
|
|
);
|
|
});
|
|
|
|
return this.getEmployeeUserRoles(organizationId, userId);
|
|
}
|
|
|
|
private async assertUserBelongsToOrganization(
|
|
organizationId: string,
|
|
userId: string,
|
|
) {
|
|
const exists = await this.userRepository
|
|
.createQueryBuilder("user")
|
|
.innerJoin(
|
|
"user.employee",
|
|
"employee",
|
|
"employee.organizationId = :organizationId AND employee.isCurrent = true",
|
|
{ organizationId },
|
|
)
|
|
.where("user.id = :userId", { userId })
|
|
.getExists();
|
|
|
|
if (!exists) {
|
|
throw new NotFoundException("user_not_found_in_organization");
|
|
}
|
|
}
|
|
|
|
private mergeEmployeesByUser(employees: Employee[]) {
|
|
const employeesByUserId = new Map<string, Employee>();
|
|
|
|
for (const employee of employees) {
|
|
const userId = employee.userId;
|
|
const employeeId = employee.id;
|
|
|
|
if (!userId) {
|
|
if (employeeId) {
|
|
employeesByUserId.set(employeeId, employee);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const existing = employeesByUserId.get(userId);
|
|
|
|
if (!existing) {
|
|
employeesByUserId.set(userId, employee);
|
|
continue;
|
|
}
|
|
|
|
const existingPositions = existing.employeePositions ?? [];
|
|
const nextPositions = employee.employeePositions ?? [];
|
|
const mergedEmployeePositions = Array.from(
|
|
new Map(
|
|
[...existingPositions, ...nextPositions].map((employeePosition) => [
|
|
employeePosition.id,
|
|
employeePosition,
|
|
]),
|
|
).values(),
|
|
);
|
|
|
|
employeesByUserId.set(userId, {
|
|
...existing,
|
|
...employee,
|
|
id: existing.id,
|
|
user: existing.user ?? employee.user,
|
|
userId,
|
|
name: existing.name ?? employee.name,
|
|
status: existing.status ?? employee.status,
|
|
employeePositions: mergedEmployeePositions,
|
|
});
|
|
}
|
|
|
|
return [...employeesByUserId.values()];
|
|
}
|
|
|
|
private async ensureOrganizationAdminAccess(
|
|
manager: EntityManager,
|
|
organizationId: string,
|
|
userId: string,
|
|
) {
|
|
const roles = await manager.getRepository(Role).find({
|
|
where: [
|
|
{ key: ORGANIZATION_ADMIN_ROLE_KEY },
|
|
{ key: EDR_ORG_MANAGER_ROLE_KEY },
|
|
],
|
|
select: { id: true, key: true },
|
|
});
|
|
|
|
const requiredRoles = [ORGANIZATION_ADMIN_ROLE_KEY, EDR_ORG_MANAGER_ROLE_KEY].map((key) => {
|
|
const role = roles.find((item) => item.key === key);
|
|
|
|
if (!role?.id) {
|
|
throw new NotFoundException(`required_role_not_seeded:${key}`);
|
|
}
|
|
|
|
return {
|
|
id: role.id,
|
|
key: role.key,
|
|
};
|
|
});
|
|
|
|
const existingRoleIds = new Set(
|
|
(
|
|
await manager.getRepository(UserRole).find({
|
|
where: {
|
|
userId,
|
|
organizationId,
|
|
},
|
|
select: { roleId: true },
|
|
})
|
|
).map((userRole) => userRole.roleId),
|
|
);
|
|
|
|
const rolesToInsert = requiredRoles
|
|
.filter((role) => !existingRoleIds.has(role.id))
|
|
.map((role) => ({
|
|
userId,
|
|
roleId: role.id,
|
|
organizationId,
|
|
}));
|
|
|
|
if (!rolesToInsert.length) {
|
|
return;
|
|
}
|
|
|
|
await manager.getRepository(UserRole).insert(rolesToInsert);
|
|
}
|
|
}
|