From ed3c526efd9e1d8f0cb3f4f4280f01077f611b64 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Mon, 21 Apr 2025 10:58:14 +0400 Subject: [PATCH] fix commissioning --- libs/common/src/constants/role-permissions.ts | 3 + .../modules/device/entities/device.entity.ts | 2 +- .../src/modules/space/entities/tag.entity.ts | 4 - .../commission-device.module.ts | 4 + .../commission-device.controller.ts | 7 +- .../services/commission-device.service.ts | 195 +++++++++++++++--- src/project/controllers/project.controller.ts | 12 +- src/project/services/project.service.ts | 39 ++-- src/space/handlers/disable-space.handler.ts | 2 +- src/space/services/space.service.ts | 4 +- src/space/services/tag/tag.service.ts | 6 +- 11 files changed, 214 insertions(+), 64 deletions(-) diff --git a/libs/common/src/constants/role-permissions.ts b/libs/common/src/constants/role-permissions.ts index cd4593c..2414f22 100644 --- a/libs/common/src/constants/role-permissions.ts +++ b/libs/common/src/constants/role-permissions.ts @@ -3,6 +3,7 @@ import { RoleType } from './role.type.enum'; export const RolePermissions = { [RoleType.SUPER_ADMIN]: [ 'DEVICE_SINGLE_CONTROL', + 'COMMISSION_DEVICE', 'DEVICE_VIEW', 'DEVICE_DELETE', 'DEVICE_UPDATE', @@ -58,6 +59,7 @@ export const RolePermissions = { 'PRODUCT_ADD', ], [RoleType.ADMIN]: [ + 'COMMISSION_DEVICE', 'DEVICE_SINGLE_CONTROL', 'DEVICE_VIEW', 'DEVICE_DELETE', @@ -127,6 +129,7 @@ export const RolePermissions = { 'SCENES_CONTROL', ], [RoleType.SPACE_OWNER]: [ + 'COMMISSION_DEVICE', 'DEVICE_SINGLE_CONTROL', 'DEVICE_VIEW', 'DEVICE_DELETE', diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 89c8f23..7ee0f7f 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -75,7 +75,7 @@ export class DeviceEntity extends AbstractEntity { @OneToMany(() => NewTagEntity, (tag) => tag.devices) // @JoinTable({ name: 'device_tags' }) - public tag: NewTagEntity[]; + public tag: NewTagEntity; constructor(partial: Partial) { super(); diff --git a/libs/common/src/modules/space/entities/tag.entity.ts b/libs/common/src/modules/space/entities/tag.entity.ts index 059208c..cfa895c 100644 --- a/libs/common/src/modules/space/entities/tag.entity.ts +++ b/libs/common/src/modules/space/entities/tag.entity.ts @@ -3,7 +3,6 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { ProductEntity } from '../../product/entities'; import { TagDto } from '../dtos'; import { TagModel } from '../../space-model/entities/tag-model.entity'; -import { SpaceEntity } from './space.entity'; import { DeviceEntity } from '../../device/entities'; import { SubspaceEntity } from './subspace/subspace.entity'; @@ -22,9 +21,6 @@ export class TagEntity extends AbstractEntity { }) product: ProductEntity; - @ManyToOne(() => SpaceEntity, (space) => space.tags, { nullable: true }) - space: SpaceEntity; - @ManyToOne(() => SubspaceEntity, (subspace) => subspace.tags, { nullable: true, }) diff --git a/src/commission-device/commission-device.module.ts b/src/commission-device/commission-device.module.ts index 62fb581..ebf0516 100644 --- a/src/commission-device/commission-device.module.ts +++ b/src/commission-device/commission-device.module.ts @@ -19,6 +19,8 @@ import { SceneRepository, } from '@app/common/modules/scene/repositories'; import { AutomationRepository } from '@app/common/modules/automation/repositories'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; @Module({ imports: [ConfigModule, SpaceRepositoryModule], @@ -39,6 +41,8 @@ import { AutomationRepository } from '@app/common/modules/automation/repositorie SceneIconRepository, SceneRepository, AutomationRepository, + CommunityRepository, + SubspaceRepository, ], exports: [], }) diff --git a/src/commission-device/controllers/commission-device.controller.ts b/src/commission-device/controllers/commission-device.controller.ts index 59f0ee8..2c715ba 100644 --- a/src/commission-device/controllers/commission-device.controller.ts +++ b/src/commission-device/controllers/commission-device.controller.ts @@ -22,9 +22,10 @@ import { ControllerRoute } from '@app/common/constants/controller-route'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { CommissionDeviceCsvDto } from '../dto'; -import { CommunityParam } from '@app/common/dto/community-space.param'; import { DeviceCommissionService } from '../services'; import { ProjectParam } from '@app/common/dto/project-param.dto'; +import { Permissions } from 'src/decorators/permissions.decorator'; +import { PermissionsGuard } from 'src/guards/permissions.guard'; @ApiTags('Commission Devices Module') @Controller({ @@ -34,6 +35,8 @@ import { ProjectParam } from '@app/common/dto/project-param.dto'; export class DeviceCommissionController { constructor(private readonly commissionService: DeviceCommissionService) {} + @UseGuards(PermissionsGuard) + @Permissions('COMMISSION_DEVICE') @ApiBearerAuth() @Post() @ApiConsumes('multipart/form-data') @@ -65,7 +68,7 @@ export class DeviceCommissionController { @Param() param: ProjectParam, @Req() req: any, ): Promise { - await this.commissionService.processCsv(file.path); + await this.commissionService.processCsv(param, file.path); return { message: 'CSV file received and processing started', success: true, diff --git a/src/commission-device/services/commission-device.service.ts b/src/commission-device/services/commission-device.service.ts index 15fc6ef..1d472bc 100644 --- a/src/commission-device/services/commission-device.service.ts +++ b/src/commission-device/services/commission-device.service.ts @@ -2,44 +2,185 @@ import * as fs from 'fs'; import * as csv from 'csv-parser'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { DeviceService } from 'src/device/services'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { SpaceRepository } from '@app/common/modules/space'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; +import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { ProjectParam } from '@app/common/dto/project-param.dto'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; @Injectable() export class DeviceCommissionService { constructor( private readonly tuyaService: TuyaService, private readonly deviceService: DeviceService, + private readonly communityRepository: CommunityRepository, + private readonly spaceRepository: SpaceRepository, + private readonly subspaceRepository: SubspaceRepository, + private readonly deviceRepository: DeviceRepository, + private readonly projectRepository: ProjectRepository, ) {} - async processCsv(filePath: string): Promise { - return new Promise((resolve, reject) => { - const results = []; + async processCsv(param: ProjectParam, filePath: string) { + const successCount = { value: 0 }; + const failureCount = { value: 0 }; - fs.createReadStream(filePath) - .pipe(csv()) - .on('data', async (row) => { - console.log(`Device: ${JSON.stringify(row)}`); - const deviceId = row.deviceId?.trim(); + const projectId = param.projectUuid; - if (!deviceId) { - console.error('Missing deviceId or deviceName in row:', row); - return; - } else { - const device = await this.tuyaService.getDeviceDetails( - row.deviceId, - ); - console.log(device); - } - }) - .on('end', () => { - console.log(`Finished processing ${results.length} devices.`); - resolve(); - }) - .on('error', (error) => { - console.error('Error reading CSV', error); - reject(error); - }); + const project = await this.projectRepository.findOne({ + where: { uuid: projectId }, }); + + if (!project) { + throw new HttpException('Project not found', HttpStatus.NOT_FOUND); + } + + const rows: any[] = []; + try { + await new Promise((resolve, reject) => { + fs.createReadStream(filePath) + .pipe(csv()) + .on('data', (row) => rows.push(row)) + .on('end', () => resolve()) + .on('error', (error) => reject(error)); + }); + + for (const row of rows) { + await this.processCsvRow(param, row, successCount, failureCount); + } + + return new SuccessResponseDto({ + message: `Successfully processed CSV file`, + data: { + successCount: successCount.value, + failureCount: failureCount.value, + }, + statusCode: HttpStatus.ACCEPTED, + }); + } catch (error) { + throw new HttpException( + 'Failed to process CSV file', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async processCsvRow( + param: ProjectParam, + row: any, + successCount: { value: number }, + failureCount: { value: number }, + ) { + try { + const rawDeviceId = row['Device ID']?.trim(); + const communityId = row['Community UUID']?.trim(); + const spaceId = row['Space UUID']?.trim(); + const subspaceId = row['Subspace UUID']?.trim(); + const tagName = row['Tag']?.trim(); + const productName = row['Product Name']?.trim(); + const projectId = param.projectUuid; + + if (!rawDeviceId) { + console.error('Missing Device ID in row:', row); + failureCount.value++; + return; + } + + const device = await this.tuyaService.getDeviceDetails(rawDeviceId); + if (!device) { + console.error(`Device not found for Device ID: ${rawDeviceId}`); + failureCount.value++; + return; + } + + const community = await this.communityRepository.findOne({ + where: { uuid: communityId, project: { uuid: projectId } }, + }); + + if (!community) { + console.error(`Community not found: ${communityId}`); + failureCount.value++; + return; + } + + const tuyaSpaceId = community.externalId; + + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceId }, + relations: [ + 'productAllocations', + 'productAllocations.tags', + 'productAllocations.product', + ], + }); + + if (!space) { + console.error(`Space not found: ${spaceId}`); + failureCount.value++; + return; + } + + let subspace: SubspaceEntity | null = null; + if (subspaceId?.trim()) { + subspace = await this.subspaceRepository.findOne({ + where: { uuid: subspaceId }, + relations: [ + 'productAllocations', + 'productAllocations.tags', + 'productAllocations.product', + ], + }); + + if (!subspace) { + console.error(`Subspace not found: ${subspaceId}`); + failureCount.value++; + return; + } + } + + const allocations = + subspace?.productAllocations || space.productAllocations; + + const match = allocations + .flatMap((pa) => + (pa.tags || []).map((tag) => ({ product: pa.product, tag })), + ) + .find(({ tag }) => tag.name === tagName); + + if (!match) { + console.error(`No matching tag found for Device ID: ${rawDeviceId}`); + failureCount.value++; + return; + } + + if (match.product.name !== productName) { + console.error(`Product name mismatch for Device ID: ${rawDeviceId}`); + failureCount.value++; + return; + } + + const middlewareDevice = this.deviceRepository.create({ + deviceTuyaUuid: rawDeviceId, + isActive: true, + spaceDevice: space, + subspace: subspace || null, + productDevice: match.product, + tag: match.tag, + }); + + await this.deviceRepository.save(middlewareDevice); + + await this.deviceService.transferDeviceInSpacesTuya( + rawDeviceId, + tuyaSpaceId, + ); + successCount.value++; + } catch (err) { + failureCount.value++; + } } } diff --git a/src/project/controllers/project.controller.ts b/src/project/controllers/project.controller.ts index 2394e47..24d5a44 100644 --- a/src/project/controllers/project.controller.ts +++ b/src/project/controllers/project.controller.ts @@ -5,6 +5,7 @@ import { Delete, Get, Header, + HttpStatus, Param, Post, Put, @@ -12,6 +13,7 @@ import { Res, UseGuards, } from '@nestjs/common'; +import { Response } from 'express'; import { ApiBearerAuth, ApiOperation, @@ -115,7 +117,13 @@ export class ProjectController { @Param() params: GetProjectParam, @Res() res: Response, ): Promise { - const csvStream = await this.projectService.exportToCsv(params); - csvStream.pipe(res as unknown as NodeJS.WritableStream); + try { + const csvStream = await this.projectService.exportToCsv(params); + csvStream.pipe(res as unknown as NodeJS.WritableStream); + } catch (error) { + res + .status(error.status || HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: error.message || 'Failed to generate CSV file.' }); + } } } diff --git a/src/project/services/project.service.ts b/src/project/services/project.service.ts index 50de089..f771c3a 100644 --- a/src/project/services/project.service.ts +++ b/src/project/services/project.service.ts @@ -254,17 +254,16 @@ export class ProjectService { const stream = new PassThrough(); const csvStream = format({ headers: [ - 'Project Name', - 'Project UUID', + 'Device ID', 'Community Name', - 'Community UUID', - 'Space Location', 'Space Name', - 'Space UUID', + 'Space Location', 'Subspace Name', - 'Subspace UUID', 'Tag', - 'Tag Product Name', + 'Product Name', + 'Community UUID', + 'Space UUID', + 'Subspace UUID', ], }); @@ -306,17 +305,16 @@ export class ProjectService { for (const productAllocation of subspace.productAllocations || []) { for (const tag of productAllocation.tags || []) { csvStream.write({ - 'Project Name': project.name, - 'Project UUID': project.uuid, + 'Device ID': '', 'Community Name': space.community?.name || '', - 'Community UUID': space.community?.uuid || '', - 'Space Location': spaceLocation, 'Space Name': space.spaceName, - 'Space UUID': space.uuid, + 'Space Location': spaceLocation, 'Subspace Name': subspace.subspaceName || '', - 'Subspace UUID': subspace.uuid, Tag: tag.name, - 'Tag Product Name': productAllocation.product.name || '', + 'Product Name': productAllocation.product.name || '', + 'Community UUID': space.community?.uuid || '', + 'Space UUID': space.uuid, + 'Subspace UUID': subspace.uuid, }); } } @@ -325,17 +323,16 @@ export class ProjectService { for (const productAllocation of space.productAllocations || []) { for (const tag of productAllocation.tags || []) { csvStream.write({ - 'Project Name': project.name, - 'Project UUID': project.uuid, + 'Device ID': '', 'Community Name': space.community?.name || '', - 'Community UUID': space.community?.uuid || '', - 'Space Location': spaceLocation, 'Space Name': space.spaceName, - 'Space UUID': space.uuid, + 'Space Location': spaceLocation, 'Subspace Name': '', - 'Subspace UUID': '', Tag: tag.name, - 'Tag Product Name': productAllocation.product.name || '', + 'Product Name': productAllocation.product.name || '', + 'Community UUID': space.community?.uuid || '', + 'Space UUID': space.uuid, + 'Subspace UUID': '', }); } } diff --git a/src/space/handlers/disable-space.handler.ts b/src/space/handlers/disable-space.handler.ts index 81a90f9..1dea9ca 100644 --- a/src/space/handlers/disable-space.handler.ts +++ b/src/space/handlers/disable-space.handler.ts @@ -65,7 +65,7 @@ export class DisableSpaceHandler } } - const tagUuids = space.tags?.map((tag) => tag.uuid) || []; + const tagUuids = space.productAllocations?.map((tag) => tag.uuid) || []; /* const subspaceDtos = space.subspaces?.map((subspace) => ({ subspaceUuid: subspace.uuid, diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 2caae61..246f583 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -582,8 +582,8 @@ export class SpaceService { queryRunner: QueryRunner, ): Promise { try { - if (space.subspaces || space.tags) { - if (space.tags) { + if (space.subspaces || space.productAllocations) { + if (space.productAllocations) { await this.spaceProductAllocationService.unlinkModels( space, queryRunner, diff --git a/src/space/services/tag/tag.service.ts b/src/space/services/tag/tag.service.ts index f40785b..1f3e977 100644 --- a/src/space/services/tag/tag.service.ts +++ b/src/space/services/tag/tag.service.ts @@ -118,7 +118,7 @@ export class TagService { await queryRunner.manager.update( this.tagRepository.target, { uuid: tag.uuid }, - { subspace, space: null }, + { subspace }, ); tag.subspace = subspace; } @@ -127,10 +127,9 @@ export class TagService { await queryRunner.manager.update( this.tagRepository.target, { uuid: tag.uuid }, - { subspace: null, space: space }, + { subspace: null }, ); tag.subspace = null; - tag.space = space; } return tag; @@ -367,7 +366,6 @@ export class TagService { where: [ { tag, - space: { uuid: spaceUuid }, product: { uuid: productUuid }, disabled: false, },