import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; import { Injectable, HttpException, HttpStatus, BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddUnitDto, AddUserUnitDto, AddUserUnitUsingCodeDto } from '../dtos'; import { UnitChildInterface, UnitParentInterface, GetUnitByUuidInterface, RenameUnitByUuidInterface, GetUnitByUserUuidInterface, } from '../interface/unit.interface'; import { SpaceEntity } from '@app/common/modules/space/entities'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { UserSpaceRepository } from '@app/common/modules/user-space/repositories'; import { generateRandomString } from '@app/common/helper/randomString'; import { UserDevicePermissionService } from 'src/user-device-permission/services'; import { PermissionType } from '@app/common/constants/permission-type.enum'; @Injectable() export class UnitService { constructor( private readonly spaceRepository: SpaceRepository, private readonly spaceTypeRepository: SpaceTypeRepository, private readonly userSpaceRepository: UserSpaceRepository, private readonly userDevicePermissionService: UserDevicePermissionService, ) {} async addUnit(addUnitDto: AddUnitDto) { try { const spaceType = await this.spaceTypeRepository.findOne({ where: { type: 'unit', }, }); const unit = await this.spaceRepository.save({ spaceName: addUnitDto.unitName, parent: { uuid: addUnitDto.floorUuid }, spaceType: { uuid: spaceType.uuid }, }); return unit; } catch (err) { throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); } } async getUnitByUuid(unitUuid: string): Promise { try { const unit = await this.spaceRepository.findOne({ where: { uuid: unitUuid, spaceType: { type: 'unit', }, }, relations: ['spaceType'], }); if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { throw new BadRequestException('Invalid unit UUID'); } return { uuid: unit.uuid, createdAt: unit.createdAt, updatedAt: unit.updatedAt, name: unit.spaceName, type: unit.spaceType.type, }; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); } } } async getUnitChildByUuid( unitUuid: string, getUnitChildDto: GetUnitChildDto, ): Promise { try { const { page, pageSize } = getUnitChildDto; const space = await this.spaceRepository.findOneOrFail({ where: { uuid: unitUuid }, relations: ['children', 'spaceType'], }); if (!space || !space.spaceType || space.spaceType.type !== 'unit') { throw new BadRequestException('Invalid unit UUID'); } const totalCount = await this.spaceRepository.count({ where: { parent: { uuid: space.uuid } }, }); const children = await this.buildHierarchy(space, false, page, pageSize); return { uuid: space.uuid, name: space.spaceName, type: space.spaceType.type, totalCount, children, }; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); } } } private async buildHierarchy( space: SpaceEntity, includeSubSpaces: boolean, page: number, pageSize: number, ): Promise { const children = await this.spaceRepository.find({ where: { parent: { uuid: space.uuid } }, relations: ['spaceType'], skip: (page - 1) * pageSize, take: pageSize, }); if (!children || children.length === 0 || !includeSubSpaces) { return children .filter( (child) => child.spaceType.type !== 'unit' && child.spaceType.type !== 'floor' && child.spaceType.type !== 'community' && child.spaceType.type !== 'unit', ) // Filter remaining unit and floor and community and unit types .map((child) => ({ uuid: child.uuid, name: child.spaceName, type: child.spaceType.type, })); } const childHierarchies = await Promise.all( children .filter( (child) => child.spaceType.type !== 'unit' && child.spaceType.type !== 'floor' && child.spaceType.type !== 'community' && child.spaceType.type !== 'unit', ) // Filter remaining unit and floor and community and unit types .map(async (child) => ({ uuid: child.uuid, name: child.spaceName, type: child.spaceType.type, children: await this.buildHierarchy(child, true, 1, pageSize), })), ); return childHierarchies; } async getUnitParentByUuid(unitUuid: string): Promise { try { const unit = await this.spaceRepository.findOne({ where: { uuid: unitUuid, spaceType: { type: 'unit', }, }, relations: ['spaceType', 'parent', 'parent.spaceType'], }); if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { throw new BadRequestException('Invalid unit UUID'); } return { uuid: unit.uuid, name: unit.spaceName, type: unit.spaceType.type, parent: { uuid: unit.parent.uuid, name: unit.parent.spaceName, type: unit.parent.spaceType.type, }, }; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); } } } async getUnitsByUserId( userUuid: string, ): Promise { try { const units = await this.userSpaceRepository.find({ relations: ['space', 'space.spaceType'], where: { user: { uuid: userUuid }, space: { spaceType: { type: 'unit' } }, }, }); if (units.length === 0) { throw new HttpException('this user has no units', HttpStatus.NOT_FOUND); } const spaces = units.map((unit) => ({ uuid: unit.space.uuid, name: unit.space.spaceName, type: unit.space.spaceType.type, })); return spaces; } catch (err) { if (err instanceof HttpException) { throw err; } else { throw new HttpException('user not found', HttpStatus.NOT_FOUND); } } } async addUserUnit(addUserUnitDto: AddUserUnitDto) { try { return await this.userSpaceRepository.save({ user: { uuid: addUserUnitDto.userUuid }, space: { uuid: addUserUnitDto.unitUuid }, }); } catch (err) { if (err.code === '23505') { throw new HttpException( 'User already belongs to this unit', HttpStatus.BAD_REQUEST, ); } throw new HttpException( err.message || 'Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR, ); } } async renameUnitByUuid( unitUuid: string, updateUnitNameDto: UpdateUnitNameDto, ): Promise { try { const unit = await this.spaceRepository.findOneOrFail({ where: { uuid: unitUuid }, relations: ['spaceType'], }); if (!unit || !unit.spaceType || unit.spaceType.type !== 'unit') { throw new BadRequestException('Invalid unit UUID'); } await this.spaceRepository.update( { uuid: unitUuid }, { spaceName: updateUnitNameDto.unitName }, ); // Fetch the updated unit const updatedUnit = await this.spaceRepository.findOneOrFail({ where: { uuid: unitUuid }, relations: ['spaceType'], }); return { uuid: updatedUnit.uuid, name: updatedUnit.spaceName, type: updatedUnit.spaceType.type, }; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); } } } async getUnitInvitationCode(unitUuid: string): Promise { try { // Generate a 6-character random invitation code const invitationCode = generateRandomString(6); // Update the unit with the new invitation code await this.spaceRepository.update({ uuid: unitUuid }, { invitationCode }); // Fetch the updated unit const updatedUnit = await this.spaceRepository.findOneOrFail({ where: { uuid: unitUuid }, relations: ['spaceType'], }); return { uuid: updatedUnit.uuid, invitationCode: updatedUnit.invitationCode, type: updatedUnit.spaceType.type, }; } catch (err) { if (err instanceof BadRequestException) { throw err; } else { throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); } } } async verifyCodeAndAddUserUnit( addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, ) { try { const unit = await this.findUnitByInviteCode( addUserUnitUsingCodeDto.inviteCode, ); await this.addUserToUnit(addUserUnitUsingCodeDto.userUuid, unit.uuid); await this.clearUnitInvitationCode(unit.uuid); const deviceUUIDs = await this.getDeviceUUIDsForUnit(unit.uuid); await this.addUserPermissionsToDevices( addUserUnitUsingCodeDto.userUuid, deviceUUIDs, ); } catch (err) { throw new HttpException( 'Invalid invitation code', HttpStatus.BAD_REQUEST, ); } } private async findUnitByInviteCode(inviteCode: string): Promise { const unit = await this.spaceRepository.findOneOrFail({ where: { invitationCode: inviteCode, spaceType: { type: 'unit' }, }, relations: ['spaceType'], }); return unit; } private async addUserToUnit(userUuid: string, unitUuid: string) { const user = await this.addUserUnit({ userUuid, unitUuid }); if (user.uuid) { return user; } else { throw new HttpException( 'Failed to add user to unit', HttpStatus.INTERNAL_SERVER_ERROR, ); } } private async clearUnitInvitationCode(unitUuid: string) { await this.spaceRepository.update( { uuid: unitUuid }, { invitationCode: null }, ); } private async getDeviceUUIDsForUnit( unitUuid: string, ): Promise<{ uuid: string }[]> { const devices = await this.spaceRepository.find({ where: { parent: { uuid: unitUuid } }, relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'], }); const allDevices = devices.flatMap((space) => space.devicesSpaceEntity); return allDevices.map((device) => ({ uuid: device.uuid })); } private async addUserPermissionsToDevices( userUuid: string, deviceUUIDs: { uuid: string }[], ): Promise { const permissionPromises = deviceUUIDs.map(async (device) => { try { await this.userDevicePermissionService.addUserPermission({ userUuid, deviceUuid: device.uuid, permissionType: PermissionType.CONTROLLABLE, }); } catch (error) { console.error( `Failed to add permission for device ${device.uuid}: ${error.message}`, ); } }); await Promise.all(permissionPromises); } }