Compare commits

...

18 Commits

Author SHA1 Message Date
b2fb378e52 Merge pull request #410 from SyncrowIOT/SP-1754-be-implement-configure-space
SP-1754-be-implement-configure-space
2025-07-02 02:26:29 -06:00
c5f8f96977 Merge branch 'dev' into SP-1754-be-implement-configure-space 2025-07-02 02:25:43 -06:00
0b9eef276e ensure Timer is the category value for CUR2 type (#448) 2025-06-30 15:52:01 +03:00
b3f8b92826 ensure Timer is the category value for CUR2 type (#446) 2025-06-30 15:35:23 +03:00
5d4e5ca87e Merge pull request #444 from SyncrowIOT/SP-1736-fe-on-user-management-page-when-i-invited-a-user-as-a-space-member-his-role-appeared-as-admin-in-the-email
SP-1736-fe-on-user-management-page-when-i-invited-a-user-as-a-space-member-his-role-appeared-as-admin-in-the-email
2025-06-30 01:23:55 -06:00
f4e748d735 fix: update role type formatting in user invitation email 2025-06-30 00:58:30 -06:00
f4f7999ae0 add device to firebase & stop moving it from the OEM space (#443) 2025-06-30 09:48:16 +03:00
82c82d521c add deviceName to handle password API (#442) 2025-06-30 08:57:43 +03:00
c7a4ff1194 fix: schedule device types (#441) 2025-06-29 15:27:55 +03:00
8a4633b158 Merge pull request #439 from SyncrowIOT/add-check-log-to-trace-the-map-issue
feat: enhance device status handling with caching and batch processin…
2025-06-25 18:59:37 -06:00
d3d84da5e3 fix: correct property name from bookableConfigs to bookableConfig in BookableSpaceEntity and SpaceEntity 2025-06-23 00:39:29 -06:00
49cc762962 fix duplication from conflict merge 2025-06-18 01:56:51 -06:00
a94d4610ed Merge branch 'dev' into SP-1754-be-implement-configure-space 2025-06-18 01:55:48 -06:00
274cdf741f refactor: streamline Booking module and service by removing unused imports and consolidating space validation logic 2025-06-18 01:49:00 -06:00
df59e9a4a3 refactor: update BookableSpaceEntity relationship to OneToOne with SpaceEntity 2025-06-18 01:48:46 -06:00
8c34c68ba6 refactor: remove BookableSpaceDto and its index export 2025-06-18 01:48:35 -06:00
332b2f5851 feat: implement Booking module with BookableSpace entity, controller, service, and DTOs for managing bookable spaces 2025-06-17 22:02:13 -06:00
8d44b66dd3 feat: add BookableSpace entity, DTO, repository, and integrate with Space entity 2025-06-17 22:02:08 -06:00
22 changed files with 489 additions and 159 deletions

View File

@ -69,7 +69,16 @@ export class ControllerRoute {
'Retrieve the list of all regions registered in Syncrow.';
};
};
static BOOKABLE_SPACES = class {
public static readonly ROUTE = 'bookable-spaces';
static ACTIONS = class {
public static readonly ADD_BOOKABLE_SPACES_SUMMARY =
'Add new bookable spaces';
public static readonly ADD_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint allows you to add new bookable spaces by providing the required details.';
};
};
static COMMUNITY = class {
public static readonly ROUTE = '/projects/:projectUuid/communities';
static ACTIONS = class {

View File

@ -58,6 +58,7 @@ import {
UserSpaceEntity,
} from '../modules/user/entities';
import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
import { BookableSpaceEntity } from '../modules/booking/entities';
@Module({
imports: [
TypeOrmModule.forRootAsync({
@ -117,6 +118,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
PresenceSensorDailySpaceEntity,
AqiSpaceDailyPollutantStatsEntity,
SpaceDailyOccupancyDurationEntity,
BookableSpaceEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

@ -0,0 +1,5 @@
// Convert time string (HH:mm) to minutes
export function timeToMinutes(time: string): number {
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BookableSpaceEntity } from './entities/bookable-space.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([BookableSpaceEntity])],
})
export class BookableRepositoryModule {}

View File

@ -0,0 +1,48 @@
import {
Entity,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToOne,
JoinColumn,
} from 'typeorm';
import { SpaceEntity } from '../../space/entities/space.entity';
import { DaysEnum } from '@app/common/constants/days.enum';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
@Entity('bookable-space')
export class BookableSpaceEntity extends AbstractEntity {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@OneToOne(() => SpaceEntity, (space) => space.bookableConfig)
@JoinColumn({ name: 'space_uuid' })
space: SpaceEntity;
@Column({
type: 'enum',
enum: DaysEnum,
array: true,
nullable: false,
})
daysAvailable: DaysEnum[];
@Column({ type: 'time' })
startTime: string;
@Column({ type: 'time' })
endTime: string;
@Column({ type: 'int' })
points: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1 @@
export * from './bookable-space.entity';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { BookableSpaceEntity } from '../entities/bookable-space.entity';
@Injectable()
export class BookableSpaceEntityRepository extends Repository<BookableSpaceEntity> {
constructor(private dataSource: DataSource) {
super(BookableSpaceEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1 @@
export * from './booking.repository';

View File

@ -1,4 +1,12 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
} from 'typeorm';
import { SpaceDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
import { CommunityEntity } from '../../community/entities';
@ -9,9 +17,9 @@ import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities';
import { SceneEntity } from '../../scene/entities';
import { SpaceModelEntity } from '../../space-model';
import { UserSpaceEntity } from '../../user/entities';
import { SpaceDto } from '../dtos';
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
import { SubspaceEntity } from './subspace/subspace.entity';
import { BookableSpaceEntity } from '../../booking/entities';
@Entity({ name: 'space' })
export class SpaceEntity extends AbstractEntity<SpaceDto> {
@ -115,6 +123,9 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
)
occupancyDaily: SpaceDailyOccupancyDurationEntity[];
@OneToOne(() => BookableSpaceEntity, (bookable) => bookable.space)
bookableConfig: BookableSpaceEntity;
constructor(partial: Partial<SpaceEntity>) {
super();
Object.assign(this, partial);

View File

@ -44,6 +44,7 @@ import { OccupancyModule } from './occupancy/occupancy.module';
import { WeatherModule } from './weather/weather.module';
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';
import { SchedulerModule } from './scheduler/scheduler.module';
import { BookingModule } from './booking';
@Module({
imports: [
ConfigModule.forRoot({
@ -98,6 +99,7 @@ import { SchedulerModule } from './scheduler/scheduler.module';
AqiModule,
SchedulerModule,
NestScheduleModule.forRoot(),
BookingModule,
],
providers: [
{

View File

@ -0,0 +1,17 @@
import { Global, Module } from '@nestjs/common';
import { BookableSpaceController } from './controllers';
import { BookableSpaceService } from './services';
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories';
import { SpaceRepository } from '@app/common/modules/space';
@Global()
@Module({
controllers: [BookableSpaceController],
providers: [
BookableSpaceService,
BookableSpaceEntityRepository,
SpaceRepository,
],
exports: [BookableSpaceService],
})
export class BookingModule {}

View File

@ -0,0 +1,30 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { CreateBookableSpaceDto } from '../dtos';
import { BookableSpaceService } from '../services';
@ApiTags('Booking Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.BOOKABLE_SPACES.ROUTE,
})
export class BookableSpaceController {
constructor(private readonly bookableSpaceService: BookableSpaceService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post()
@ApiOperation({
summary:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_DESCRIPTION,
})
async create(@Body() dto: CreateBookableSpaceDto): Promise<BaseResponseDto> {
return this.bookableSpaceService.create(dto);
}
}

View File

@ -0,0 +1 @@
export * from './bookable-space.controller';

View File

@ -0,0 +1,62 @@
// dtos/bookable-space.dto.ts
import { DaysEnum } from '@app/common/constants/days.enum';
import { ApiProperty } from '@nestjs/swagger';
import {
IsArray,
IsEnum,
IsNotEmpty,
IsString,
IsUUID,
IsInt,
ArrayMinSize,
Max,
Min,
Matches,
} from 'class-validator';
export class CreateBookableSpaceDto {
@ApiProperty({
type: 'string',
isArray: true,
example: [
'3fa85f64-5717-4562-b3fc-2c963f66afa6',
'4fa85f64-5717-4562-b3fc-2c963f66afa7',
],
})
@IsArray()
@ArrayMinSize(1, { message: 'At least one space must be selected' })
@IsUUID('all', { each: true, message: 'Invalid space UUID provided' })
spaceUuids: string[];
@ApiProperty({
enum: DaysEnum,
isArray: true,
example: [DaysEnum.MON, DaysEnum.WED, DaysEnum.FRI],
})
@IsArray()
@ArrayMinSize(1, { message: 'At least one day must be selected' })
@IsEnum(DaysEnum, { each: true, message: 'Invalid day provided' })
daysAvailable: DaysEnum[];
@ApiProperty({ example: '09:00' })
@IsString()
@IsNotEmpty({ message: 'Start time cannot be empty' })
@Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, {
message: 'Start time must be in HH:mm format (24-hour)',
})
startTime: string;
@ApiProperty({ example: '17:00' })
@IsString()
@IsNotEmpty({ message: 'End time cannot be empty' })
@Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, {
message: 'End time must be in HH:mm format (24-hour)',
})
endTime: string;
@ApiProperty({ example: 10 })
@IsInt()
@Min(0, { message: 'Points cannot be negative' })
@Max(1000, { message: 'Points cannot exceed 1000' })
points: number;
}

View File

@ -0,0 +1 @@
export * from './create-bookable-space.dto';

1
src/booking/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './booking.module';

View File

@ -0,0 +1,127 @@
import {
Injectable,
NotFoundException,
ConflictException,
BadRequestException,
} from '@nestjs/common';
import { CreateBookableSpaceDto } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories';
import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository';
import { In } from 'typeorm';
import { timeToMinutes } from '@app/common/helper/timeToMinutes';
@Injectable()
export class BookableSpaceService {
constructor(
private readonly bookableSpaceEntityRepository: BookableSpaceEntityRepository,
private readonly spaceRepository: SpaceRepository,
) {}
async create(dto: CreateBookableSpaceDto): Promise<BaseResponseDto> {
// Validate time slots first
this.validateTimeSlot(dto.startTime, dto.endTime);
// fetch spaces exist
const spaces = await this.getSpacesOrFindMissing(dto.spaceUuids);
// Validate no duplicate bookable configurations
await this.validateNoDuplicateBookableConfigs(dto.spaceUuids);
// Create and save bookable spaces
return this.createBookableSpaces(spaces, dto);
}
/**
* Fetch spaces by UUIDs and throw an error if any are missing
*/
private async getSpacesOrFindMissing(
spaceUuids: string[],
): Promise<SpaceEntity[]> {
const spaces = await this.spaceRepository.find({
where: { uuid: In(spaceUuids) },
});
if (spaces.length !== spaceUuids.length) {
const foundUuids = spaces.map((s) => s.uuid);
const missingUuids = spaceUuids.filter(
(uuid) => !foundUuids.includes(uuid),
);
throw new NotFoundException(
`Spaces not found: ${missingUuids.join(', ')}`,
);
}
return spaces;
}
/**
* Validate there are no existing bookable configurations for these spaces
*/
private async validateNoDuplicateBookableConfigs(
spaceUuids: string[],
): Promise<void> {
const existingBookables = await this.bookableSpaceEntityRepository.find({
where: { space: { uuid: In(spaceUuids) } },
relations: ['space'],
});
if (existingBookables.length > 0) {
const existingUuids = [
...new Set(existingBookables.map((b) => b.space.uuid)),
];
throw new ConflictException(
`Bookable configuration already exists for spaces: ${existingUuids.join(', ')}`,
);
}
}
/**
* Ensure the slot start time is before the end time
*/
private validateTimeSlot(startTime: string, endTime: string): void {
const start = timeToMinutes(startTime);
const end = timeToMinutes(endTime);
if (start >= end) {
throw new BadRequestException(
`End time must be after start time for slot: ${startTime}-${endTime}`,
);
}
}
/**
* Create bookable space entries after all validations pass
*/
private async createBookableSpaces(
spaces: SpaceEntity[],
dto: CreateBookableSpaceDto,
): Promise<BaseResponseDto> {
try {
const entries = spaces.map((space) =>
this.bookableSpaceEntityRepository.create({
space,
daysAvailable: dto.daysAvailable,
startTime: dto.startTime,
endTime: dto.endTime,
points: dto.points,
}),
);
const data = await this.bookableSpaceEntityRepository.save(entries);
return new SuccessResponseDto({
data,
message: 'Successfully added new bookable spaces',
});
} catch (error) {
if (error.code === '23505') {
throw new ConflictException(
'Duplicate bookable space configuration detected',
);
}
throw error;
}
}
}

View File

@ -0,0 +1 @@
export * from './bookable-space.service';

View File

@ -3,6 +3,7 @@ import * as fs from 'fs';
import { ProjectParam } from '@app/common/dto/project-param.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { DeviceRepository } from '@app/common/modules/device/repositories';
@ -20,6 +21,7 @@ export class DeviceCommissionService {
constructor(
private readonly tuyaService: TuyaService,
private readonly deviceService: DeviceService,
private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService,
private readonly communityRepository: CommunityRepository,
private readonly spaceRepository: SpaceRepository,
private readonly subspaceRepository: SubspaceRepository,
@ -209,6 +211,10 @@ export class DeviceCommissionService {
rawDeviceId,
tuyaSpaceId,
);
await this.deviceStatusFirebaseService.addDeviceStatusByDeviceUuid(
rawDeviceId,
);
successCount.value++;
console.log(
`Device ${rawDeviceId} successfully processed and transferred to Tuya space ${tuyaSpaceId}`,

View File

@ -111,6 +111,7 @@ export class InviteUserService {
});
const invitedUser = await queryRunner.manager.save(inviteUser);
const invitedRoleType = await this.getRoleTypeByUuid(roleUuid);
// Link user to spaces
const spacePromises = validSpaces.map(async (space) => {
@ -128,7 +129,7 @@ export class InviteUserService {
await this.emailService.sendEmailWithInvitationTemplate(email, {
name: firstName,
invitationCode,
role: roleType,
role: invitedRoleType.replace(/_/g, ' '),
spacesList: spaceNames,
});

View File

@ -50,7 +50,7 @@ export class ScheduleService {
// Corrected condition for supported device types
this.ensureProductTypeSupportedForSchedule(
ProductType[deviceDetails.productDevice.prodType],
deviceDetails.productDevice.prodType as ProductType,
);
return this.enableScheduleDeviceInTuya(
@ -74,7 +74,7 @@ export class ScheduleService {
// Corrected condition for supported device types
this.ensureProductTypeSupportedForSchedule(
ProductType[deviceDetails.productDevice.prodType],
deviceDetails.productDevice.prodType as ProductType,
);
return await this.deleteScheduleDeviceInTuya(
@ -96,13 +96,24 @@ export class ScheduleService {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
if (
deviceDetails.productDevice.prodType == ProductType.CUR_2 &&
addScheduleDto.category != 'Timer'
) {
throw new HttpException(
'Invalid category for CUR_2 devices',
HttpStatus.BAD_REQUEST,
);
}
this.ensureProductTypeSupportedForSchedule(
ProductType[deviceDetails.productDevice.prodType],
deviceDetails.productDevice.prodType as ProductType,
);
await this.addScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
addScheduleDto,
deviceDetails.productDevice.prodType as ProductType,
);
} catch (error) {
throw new HttpException(
@ -120,16 +131,19 @@ export class ScheduleService {
}
// Corrected condition for supported device types
this.ensureProductTypeSupportedForSchedule(
ProductType[deviceDetails.productDevice.prodType],
deviceDetails.productDevice.prodType as ProductType,
);
const schedules = await this.getScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
category,
deviceDetails.productDevice.prodType as ProductType,
);
const result = schedules.result.map((schedule: any) => {
return {
category: schedule.category.replace('category_', ''),
category:
deviceDetails.productDevice.prodType == ProductType.CUR_2
? schedule.category
: schedule.category.replace('category_', ''),
enable: schedule.enable,
function: {
code: schedule.functions[0].code,
@ -160,14 +174,25 @@ export class ScheduleService {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
if (
deviceDetails.productDevice.prodType == ProductType.CUR_2 &&
updateScheduleDto.category != 'Timer'
) {
throw new HttpException(
'Invalid category for CUR_2 devices',
HttpStatus.BAD_REQUEST,
);
}
// Corrected condition for supported device types
this.ensureProductTypeSupportedForSchedule(
ProductType[deviceDetails.productDevice.prodType],
deviceDetails.productDevice.prodType as ProductType,
);
await this.updateScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
updateScheduleDto,
deviceDetails.productDevice.prodType as ProductType,
);
} catch (error) {
throw new HttpException(
@ -193,6 +218,7 @@ export class ScheduleService {
private async addScheduleDeviceInTuya(
deviceId: string,
addScheduleDto: AddScheduleDto,
deviceType: ProductType,
): Promise<addScheduleDeviceInterface> {
try {
const convertedTime = convertTimestampToDubaiTime(addScheduleDto.time);
@ -211,7 +237,10 @@ export class ScheduleService {
...addScheduleDto.function,
},
],
category: `category_${addScheduleDto.category}`,
category:
deviceType == ProductType.CUR_2
? addScheduleDto.category
: `category_${addScheduleDto.category}`,
},
});
@ -227,9 +256,12 @@ export class ScheduleService {
private async getScheduleDeviceInTuya(
deviceId: string,
category: string,
deviceType: ProductType,
): Promise<getDeviceScheduleInterface> {
try {
const path = `/v2.0/cloud/timer/device/${deviceId}?category=category_${category}`;
const categoryToSent =
deviceType == ProductType.CUR_2 ? category : `category_${category}`;
const path = `/v2.0/cloud/timer/device/${deviceId}?category=${categoryToSent}`;
const response = await this.tuya.request({
method: 'GET',
path,
@ -249,6 +281,7 @@ export class ScheduleService {
private async updateScheduleDeviceInTuya(
deviceId: string,
updateScheduleDto: UpdateScheduleDto,
deviceType: ProductType,
): Promise<addScheduleDeviceInterface> {
try {
const convertedTime = convertTimestampToDubaiTime(updateScheduleDto.time);
@ -269,7 +302,10 @@ export class ScheduleService {
value: updateScheduleDto.function.value,
},
],
category: `category_${updateScheduleDto.category}`,
category:
deviceType == ProductType.CUR_2
? updateScheduleDto.category
: `category_${updateScheduleDto.category}`,
},
});

View File

@ -1,39 +1,39 @@
import { VisitorPasswordRepository } from './../../../libs/common/src/modules/visitor-password/repositories/visitor-password.repository';
import { ProductType } from '@app/common/constants/product-type.enum';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import {
Injectable,
BadRequestException,
HttpException,
HttpStatus,
BadRequestException,
Injectable,
} from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import {
addDeviceObjectInterface,
createTickInterface,
} from '../interfaces/visitor-password.interface';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { ProductType } from '@app/common/constants/product-type.enum';
import { VisitorPasswordRepository } from './../../../libs/common/src/modules/visitor-password/repositories/visitor-password.repository';
import { AddDoorLockTemporaryPasswordDto } from '../dtos';
import { EmailService } from '@app/common/util/email.service';
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
import { DoorLockService } from 'src/door-lock/services';
import { DeviceService } from 'src/device/services';
import { DeviceStatuses } from '@app/common/constants/device-status.enum';
import {
DaysEnum,
EnableDisableStatusEnum,
} from '@app/common/constants/days.enum';
import { PasswordType } from '@app/common/constants/password-type.enum';
import { DeviceStatuses } from '@app/common/constants/device-status.enum';
import {
CommonHourMinutes,
CommonHours,
} from '@app/common/constants/hours-minutes.enum';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant';
import { PasswordType } from '@app/common/constants/password-type.enum';
import { VisitorPasswordEnum } from '@app/common/constants/visitor-password.enum';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { EmailService } from '@app/common/util/email.service';
import { DeviceService } from 'src/device/services';
import { DoorLockService } from 'src/door-lock/services';
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';
import { Not } from 'typeorm';
import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant';
import { AddDoorLockTemporaryPasswordDto } from '../dtos';
@Injectable()
export class VisitorPasswordService {
@ -57,6 +57,67 @@ export class VisitorPasswordService {
secretKey,
});
}
async getPasswords(projectUuid: string) {
await this.validateProject(projectUuid);
const deviceIds = await this.deviceRepository.find({
where: {
productDevice: {
prodType: ProductType.DL,
},
spaceDevice: {
spaceName: Not(ORPHAN_SPACE_NAME),
community: {
project: {
uuid: projectUuid,
},
},
},
isActive: true,
},
});
const data = [];
deviceIds.forEach((deviceId) => {
data.push(
this.doorLockService
.getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true, false)
.catch(() => {}),
this.doorLockService
.getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true, true)
.catch(() => {}),
this.doorLockService
.getOnlineTemporaryPasswordsMultiple(deviceId.uuid, true, false)
.catch(() => {}),
this.doorLockService
.getOnlineTemporaryPasswordsMultiple(deviceId.uuid, true, true)
.catch(() => {}),
this.doorLockService
.getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true, false)
.catch(() => {}),
this.doorLockService
.getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true, true)
.catch(() => {}),
this.doorLockService
.getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true, false)
.catch(() => {}),
this.doorLockService
.getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true, true)
.catch(() => {}),
);
});
const result = (await Promise.all(data)).flat().filter((datum) => {
return datum != null;
});
return new SuccessResponseDto({
message: 'Successfully retrieved temporary passwords',
data: result,
statusCode: HttpStatus.OK,
});
}
async handleTemporaryPassword(
addDoorLockTemporaryPasswordDto: AddDoorLockTemporaryPasswordDto,
userUuid: string,
@ -105,7 +166,7 @@ export class VisitorPasswordService {
statusCode: HttpStatus.CREATED,
});
}
async addOfflineMultipleTimeTemporaryPassword(
private async addOfflineMultipleTimeTemporaryPassword(
addDoorLockOfflineMultipleDto: AddDoorLockTemporaryPasswordDto,
userUuid: string,
projectUuid: string,
@ -169,6 +230,7 @@ export class VisitorPasswordService {
success: true,
result: createMultipleOfflinePass.result,
deviceUuid,
deviceName: deviceDetails.name,
};
} catch (error) {
return {
@ -231,7 +293,7 @@ export class VisitorPasswordService {
}
}
async addOfflineOneTimeTemporaryPassword(
private async addOfflineOneTimeTemporaryPassword(
addDoorLockOfflineOneTimeDto: AddDoorLockTemporaryPasswordDto,
userUuid: string,
projectUuid: string,
@ -295,6 +357,7 @@ export class VisitorPasswordService {
success: true,
result: createOnceOfflinePass.result,
deviceUuid,
deviceName: deviceDetails.name,
};
} catch (error) {
return {
@ -357,7 +420,7 @@ export class VisitorPasswordService {
}
}
async addOfflineTemporaryPasswordTuya(
private async addOfflineTemporaryPasswordTuya(
doorLockUuid: string,
type: string,
addDoorLockOfflineMultipleDto: AddDoorLockTemporaryPasswordDto,
@ -387,7 +450,7 @@ export class VisitorPasswordService {
);
}
}
async addOnlineTemporaryPasswordMultipleTime(
private async addOnlineTemporaryPasswordMultipleTime(
addDoorLockOnlineMultipleDto: AddDoorLockTemporaryPasswordDto,
userUuid: string,
projectUuid: string,
@ -448,6 +511,7 @@ export class VisitorPasswordService {
success: true,
id: createPass.result.id,
deviceUuid,
deviceName: passwordData.deviceName,
};
} catch (error) {
return {
@ -508,67 +572,8 @@ export class VisitorPasswordService {
);
}
}
async getPasswords(projectUuid: string) {
await this.validateProject(projectUuid);
const deviceIds = await this.deviceRepository.find({
where: {
productDevice: {
prodType: ProductType.DL,
},
spaceDevice: {
spaceName: Not(ORPHAN_SPACE_NAME),
community: {
project: {
uuid: projectUuid,
},
},
},
isActive: true,
},
});
const data = [];
deviceIds.forEach((deviceId) => {
data.push(
this.doorLockService
.getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true, false)
.catch(() => {}),
this.doorLockService
.getOnlineTemporaryPasswordsOneTime(deviceId.uuid, true, true)
.catch(() => {}),
this.doorLockService
.getOnlineTemporaryPasswordsMultiple(deviceId.uuid, true, false)
.catch(() => {}),
this.doorLockService
.getOnlineTemporaryPasswordsMultiple(deviceId.uuid, true, true)
.catch(() => {}),
this.doorLockService
.getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true, false)
.catch(() => {}),
this.doorLockService
.getOfflineOneTimeTemporaryPasswords(deviceId.uuid, true, true)
.catch(() => {}),
this.doorLockService
.getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true, false)
.catch(() => {}),
this.doorLockService
.getOfflineMultipleTimeTemporaryPasswords(deviceId.uuid, true, true)
.catch(() => {}),
);
});
const result = (await Promise.all(data)).flat().filter((datum) => {
return datum != null;
});
return new SuccessResponseDto({
message: 'Successfully retrieved temporary passwords',
data: result,
statusCode: HttpStatus.OK,
});
}
async addOnlineTemporaryPasswordOneTime(
private async addOnlineTemporaryPasswordOneTime(
addDoorLockOnlineOneTimeDto: AddDoorLockTemporaryPasswordDto,
userUuid: string,
projectUuid: string,
@ -627,6 +632,7 @@ export class VisitorPasswordService {
return {
success: true,
id: createPass.result.id,
deviceName: passwordData.deviceName,
deviceUuid,
};
} catch (error) {
@ -688,7 +694,7 @@ export class VisitorPasswordService {
);
}
}
async getTicketAndEncryptedPassword(
private async getTicketAndEncryptedPassword(
doorLockUuid: string,
passwordPlan: string,
projectUuid: string,
@ -725,6 +731,7 @@ export class VisitorPasswordService {
ticketKey: ticketDetails.result.ticket_key,
encryptedPassword: decrypted,
deviceTuyaUuid: deviceDetails.deviceTuyaUuid,
deviceName: deviceDetails.name,
};
} catch (error) {
throw new HttpException(
@ -734,7 +741,7 @@ export class VisitorPasswordService {
}
}
async createDoorLockTicketTuya(
private async createDoorLockTicketTuya(
deviceUuid: string,
): Promise<createTickInterface> {
try {
@ -753,7 +760,7 @@ export class VisitorPasswordService {
}
}
async addOnlineTemporaryPasswordMultipleTuya(
private async addOnlineTemporaryPasswordMultipleTuya(
addDeviceObj: addDeviceObjectInterface,
doorLockUuid: string,
): Promise<createTickInterface> {
@ -795,7 +802,7 @@ export class VisitorPasswordService {
}
}
getWorkingDayValue(days) {
private getWorkingDayValue(days) {
// Array representing the days of the week
const weekDays = [
DaysEnum.SAT,
@ -827,36 +834,7 @@ export class VisitorPasswordService {
return workingDayValue;
}
getDaysFromWorkingDayValue(workingDayValue) {
// Array representing the days of the week
const weekDays = [
DaysEnum.SAT,
DaysEnum.FRI,
DaysEnum.THU,
DaysEnum.WED,
DaysEnum.TUE,
DaysEnum.MON,
DaysEnum.SUN,
];
// Convert the integer to a binary string and pad with leading zeros to ensure 7 bits
const binaryString = workingDayValue
.toString(2)
.padStart(7, EnableDisableStatusEnum.DISABLED);
// Initialize an array to hold the days of the week
const days = [];
// Iterate through the binary string and weekDays array
for (let i = 0; i < binaryString.length; i++) {
if (binaryString[i] === EnableDisableStatusEnum.ENABLED) {
days.push(weekDays[i]);
}
}
return days;
}
timeToMinutes(timeStr) {
private timeToMinutes(timeStr) {
try {
// Special case for "24:00"
if (timeStr === CommonHours.TWENTY_FOUR) {
@ -883,38 +861,7 @@ export class VisitorPasswordService {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
minutesToTime(totalMinutes) {
try {
if (
typeof totalMinutes !== 'number' ||
totalMinutes < 0 ||
totalMinutes > CommonHourMinutes.TWENTY_FOUR
) {
throw new Error('Invalid minutes value');
}
if (totalMinutes === CommonHourMinutes.TWENTY_FOUR) {
return CommonHours.TWENTY_FOUR;
}
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const formattedHours = String(hours).padStart(
2,
EnableDisableStatusEnum.DISABLED,
);
const formattedMinutes = String(minutes).padStart(
2,
EnableDisableStatusEnum.DISABLED,
);
return `${formattedHours}:${formattedMinutes}`;
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async getDeviceByDeviceUuid(
private async getDeviceByDeviceUuid(
deviceUuid: string,
withProductDevice: boolean = true,
projectUuid: string,
@ -939,7 +886,7 @@ export class VisitorPasswordService {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
}
async addOnlineTemporaryPasswordOneTimeTuya(
private async addOnlineTemporaryPasswordOneTimeTuya(
addDeviceObj: addDeviceObjectInterface,
doorLockUuid: string,
): Promise<createTickInterface> {