Compare commits

..

1 Commits

Author SHA1 Message Date
7ea53feddc add deviceName to handle password API 2025-06-30 08:54:25 +03:00
21 changed files with 7 additions and 391 deletions

View File

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

View File

@ -1,5 +0,0 @@
// 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

@ -1,11 +0,0 @@
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

@ -1,48 +0,0 @@
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

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

View File

@ -1,10 +0,0 @@
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

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

View File

@ -1,12 +1,4 @@
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
} from 'typeorm';
import { SpaceDto } from '../dtos';
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
import { CommunityEntity } from '../../community/entities';
@ -17,9 +9,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> {
@ -123,9 +115,6 @@ 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,7 +44,6 @@ 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({
@ -99,7 +98,6 @@ import { BookingModule } from './booking';
AqiModule,
SchedulerModule,
NestScheduleModule.forRoot(),
BookingModule,
],
providers: [
{

View File

@ -1,17 +0,0 @@
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

@ -1,30 +0,0 @@
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

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

View File

@ -1,62 +0,0 @@
// 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

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

View File

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

View File

@ -1,127 +0,0 @@
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

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

View File

@ -3,7 +3,6 @@ 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';
@ -21,7 +20,6 @@ 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,
@ -211,10 +209,6 @@ 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,7 +111,6 @@ 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) => {
@ -129,7 +128,7 @@ export class InviteUserService {
await this.emailService.sendEmailWithInvitationTemplate(email, {
name: firstName,
invitationCode,
role: invitedRoleType.replace(/_/g, ' '),
role: roleType,
spacesList: spaceNames,
});

View File

@ -96,16 +96,6 @@ 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(
deviceDetails.productDevice.prodType as ProductType,
);
@ -113,7 +103,6 @@ export class ScheduleService {
await this.addScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
addScheduleDto,
deviceDetails.productDevice.prodType as ProductType,
);
} catch (error) {
throw new HttpException(
@ -136,14 +125,10 @@ export class ScheduleService {
const schedules = await this.getScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
category,
deviceDetails.productDevice.prodType as ProductType,
);
const result = schedules.result.map((schedule: any) => {
return {
category:
deviceDetails.productDevice.prodType == ProductType.CUR_2
? schedule.category
: schedule.category.replace('category_', ''),
category: schedule.category.replace('category_', ''),
enable: schedule.enable,
function: {
code: schedule.functions[0].code,
@ -174,16 +159,6 @@ 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(
deviceDetails.productDevice.prodType as ProductType,
@ -192,7 +167,6 @@ export class ScheduleService {
await this.updateScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
updateScheduleDto,
deviceDetails.productDevice.prodType as ProductType,
);
} catch (error) {
throw new HttpException(
@ -218,7 +192,6 @@ export class ScheduleService {
private async addScheduleDeviceInTuya(
deviceId: string,
addScheduleDto: AddScheduleDto,
deviceType: ProductType,
): Promise<addScheduleDeviceInterface> {
try {
const convertedTime = convertTimestampToDubaiTime(addScheduleDto.time);
@ -237,10 +210,7 @@ export class ScheduleService {
...addScheduleDto.function,
},
],
category:
deviceType == ProductType.CUR_2
? addScheduleDto.category
: `category_${addScheduleDto.category}`,
category: `category_${addScheduleDto.category}`,
},
});
@ -256,12 +226,9 @@ export class ScheduleService {
private async getScheduleDeviceInTuya(
deviceId: string,
category: string,
deviceType: ProductType,
): Promise<getDeviceScheduleInterface> {
try {
const categoryToSent =
deviceType == ProductType.CUR_2 ? category : `category_${category}`;
const path = `/v2.0/cloud/timer/device/${deviceId}?category=${categoryToSent}`;
const path = `/v2.0/cloud/timer/device/${deviceId}?category=category_${category}`;
const response = await this.tuya.request({
method: 'GET',
path,
@ -281,7 +248,6 @@ export class ScheduleService {
private async updateScheduleDeviceInTuya(
deviceId: string,
updateScheduleDto: UpdateScheduleDto,
deviceType: ProductType,
): Promise<addScheduleDeviceInterface> {
try {
const convertedTime = convertTimestampToDubaiTime(updateScheduleDto.time);
@ -302,10 +268,7 @@ export class ScheduleService {
value: updateScheduleDto.function.value,
},
],
category:
deviceType == ProductType.CUR_2
? updateScheduleDto.category
: `category_${updateScheduleDto.category}`,
category: `category_${updateScheduleDto.category}`,
},
});