feat: add occupancy module with controller, service, and related DTOs for heat map data retrieval

This commit is contained in:
faris Aljohari
2025-05-12 02:15:57 +03:00
parent c0a6e9ab63
commit 7ec4171e1a
25 changed files with 270 additions and 9 deletions

View File

@ -504,6 +504,17 @@ export class ControllerRoute {
'This endpoint retrieves the historical data of power clamp devices based on the provided community or space UUID.'; 'This endpoint retrieves the historical data of power clamp devices based on the provided community or space UUID.';
}; };
}; };
static Occupancy = class {
public static readonly ROUTE = 'occupancy';
static ACTIONS = class {
public static readonly GET_OCCUPANCY_HEAT_MAP_SUMMARY =
'Get occupancy heat map data';
public static readonly GET_OCCUPANCY_HEAT_MAP_DESCRIPTION =
'This endpoint retrieves the occupancy heat map data based on the provided parameters.';
};
};
static DEVICE = class { static DEVICE = class {
public static readonly ROUTE = 'devices'; public static readonly ROUTE = 'devices';

View File

@ -0,0 +1,4 @@
export enum PresenceSensorEnum {
PRESENCE_STATE = 'presence_state',
SENSITIVITY = 'sensitivity',
}

View File

@ -10,6 +10,7 @@ import {
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Module({ @Module({
providers: [ providers: [
@ -21,6 +22,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
controllers: [DeviceStatusFirebaseController], controllers: [DeviceStatusFirebaseController],
exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository], exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository],

View File

@ -21,6 +21,8 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log
import { ProductType } from '@app/common/constants/product-type.enum'; import { ProductType } from '@app/common/constants/product-type.enum';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.enum'; import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.enum';
import { PresenceSensorEnum } from '@app/common/constants/presence.sensor.enum';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Injectable() @Injectable()
export class DeviceStatusFirebaseService { export class DeviceStatusFirebaseService {
private tuya: TuyaContext; private tuya: TuyaContext;
@ -29,6 +31,7 @@ export class DeviceStatusFirebaseService {
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly deviceRepository: DeviceRepository, private readonly deviceRepository: DeviceRepository,
private readonly powerClampService: PowerClampService, private readonly powerClampService: PowerClampService,
private readonly occupancyService: OccupancyService,
private deviceStatusLogRepository: DeviceStatusLogRepository, private deviceStatusLogRepository: DeviceStatusLogRepository,
) { ) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY'); const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
@ -240,6 +243,23 @@ export class DeviceStatusFirebaseService {
} }
} }
if (
addDeviceStatusDto.productType === ProductType.CPS ||
addDeviceStatusDto.productType === ProductType.WPS
) {
const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]);
const occupancyStatus = addDeviceStatusDto?.log?.properties?.find(
(status) => occupancyCodes.has(status.code),
);
if (occupancyStatus) {
await this.occupancyService.updateOccupancySensorHistoricalData(
addDeviceStatusDto.deviceUuid,
);
}
}
// Return the updated data // Return the updated data
const snapshot: DataSnapshot = await get(dataRef); const snapshot: DataSnapshot = await get(dataRef);
return snapshot.val(); return snapshot.val();

View File

@ -0,0 +1,49 @@
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { Injectable } from '@nestjs/common';
import { SqlLoaderService } from './sql-loader.service';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
@Injectable()
export class OccupancyService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
private readonly deviceRepository: DeviceRepository,
) {}
async updateOccupancySensorHistoricalData(deviceUuid: string): Promise<void> {
try {
const now = new Date();
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
const device = await this.deviceRepository.findOne({
where: { uuid: deviceUuid },
relations: ['spaceDevice'],
});
await this.executeProcedure('procedure_update_fact_space_occupancy', [
dateStr,
device.spaceDevice?.uuid,
]);
} catch (err) {
console.error('Failed to insert or update occupancy data:', err);
throw err;
}
}
private async executeProcedure(
procedureFileName: string,
params: (string | number | null)[],
): Promise<void> {
const query = this.loadQuery(
'fact_space_occupancy_count',
procedureFileName,
);
await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
}
private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
}
}

View File

@ -1,12 +1,15 @@
WITH params AS ( WITH params AS (
SELECT SELECT
$1::uuid AS space_id, $1::uuid AS space_id,
TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month TO_DATE(NULLIF($2, ''), 'YYYY') AS event_year
) )
select psdsd.* SELECT psdsd.*
from public."presence-sensor-daily-space-detection" psdsd FROM public."presence-sensor-daily-space-detection" psdsd
JOIN params P ON true JOIN params P ON true
where psdsd.space_uuid = P.space_id WHERE psdsd.space_uuid = P.space_id
AND (P.event_month IS NULL OR TO_CHAR(psdsd.event_date, 'YYYY-MM') = TO_CHAR(P.event_month, 'YYYY-MM')) AND (
P.event_year IS NULL
OR TO_CHAR(psdsd.event_date, 'YYYY') = TO_CHAR(P.event_year, 'YYYY')
)
ORDER BY space_uuid, event_date ORDER BY space_uuid, event_date

View File

@ -1,7 +1,7 @@
WITH params AS ( WITH params AS (
SELECT SELECT
TO_DATE(NULLIF($2, ''), 'YYYY-MM-DD') AS event_date, TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date,
$4::uuid AS space_id $2::uuid AS space_id
), ),
device_logs AS ( device_logs AS (

View File

@ -37,12 +37,13 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
import { OccupancyModule } from './occupancy/occupancy.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
load: config, load: config,
}), }),
/* ThrottlerModule.forRoot({ /* ThrottlerModule.forRoot({
throttlers: [{ ttl: 100000, limit: 30 }], throttlers: [{ ttl: 100000, limit: 30 }],
}), */ }), */
WinstonModule.forRoot(winstonLoggerOptions), WinstonModule.forRoot(winstonLoggerOptions),
@ -77,13 +78,14 @@ import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston
DeviceCommissionModule, DeviceCommissionModule,
PowerClampModule, PowerClampModule,
HealthModule, HealthModule,
OccupancyModule,
], ],
providers: [ providers: [
{ {
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor, useClass: LoggingInterceptor,
}, },
/* { /* {
provide: APP_GUARD, provide: APP_GUARD,
useClass: ThrottlerGuard, useClass: ThrottlerGuard,
}, */ }, */

View File

@ -28,6 +28,7 @@ import {
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Module({ @Module({
imports: [ConfigModule, SpaceRepositoryModule], imports: [ConfigModule, SpaceRepositoryModule],
@ -55,6 +56,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
exports: [], exports: [],
}) })

View File

@ -63,6 +63,7 @@ import {
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Module({ @Module({
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
@ -114,6 +115,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
exports: [CommunityService, SpacePermissionService], exports: [CommunityService, SpacePermissionService],
}) })

View File

@ -27,6 +27,7 @@ import {
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Module({ @Module({
imports: [ConfigModule, DeviceRepositoryModule], imports: [ConfigModule, DeviceRepositoryModule],
controllers: [DoorLockController], controllers: [DoorLockController],
@ -52,6 +53,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
exports: [DoorLockService], exports: [DoorLockService],
}) })

View File

@ -25,6 +25,7 @@ import {
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Module({ @Module({
imports: [ConfigModule, DeviceRepositoryModule], imports: [ConfigModule, DeviceRepositoryModule],
controllers: [GroupController], controllers: [GroupController],
@ -49,6 +50,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
exports: [GroupService], exports: [GroupService],
}) })

View File

@ -83,6 +83,7 @@ import {
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Module({ @Module({
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule], imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
@ -152,6 +153,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
exports: [InviteUserService], exports: [InviteUserService],
}) })

View File

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

View File

@ -0,0 +1,46 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import {
ApiTags,
ApiBearerAuth,
ApiOperation,
ApiParam,
} from '@nestjs/swagger';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { OccupancyService } from '../services/occupancy.service';
import { GetOccupancyHeatMapBySpaceDto } from '../dto/get-occupancy.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SpaceParamsDto } from '../dto/occupancy-params.dto';
@ApiTags('Occupancy Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.Occupancy.ROUTE,
})
export class OccupancyController {
constructor(private readonly occupancyService: OccupancyService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('heat-map/space/:spaceUuid')
@ApiOperation({
summary: ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_SUMMARY,
description:
ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_DESCRIPTION,
})
@ApiParam({
name: 'spaceUuid',
description: 'UUID of the Space',
required: true,
})
async getOccupancyHeatMapDataBySpace(
@Param() params: SpaceParamsDto,
@Query() query: GetOccupancyHeatMapBySpaceDto,
): Promise<BaseResponseDto> {
return await this.occupancyService.getOccupancyHeatMapDataBySpace(
params,
query,
);
}
}

View File

@ -0,0 +1,15 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Matches, IsNotEmpty } from 'class-validator';
export class GetOccupancyHeatMapBySpaceDto {
@ApiPropertyOptional({
description: 'Input year in YYYY format to filter the data',
example: '2025',
required: false,
})
@IsNotEmpty()
@Matches(/^\d{4}$/, {
message: 'Year must be in YYYY format',
})
year: string;
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsUUID } from 'class-validator';
export class SpaceParamsDto {
@IsUUID('4', { message: 'Invalid UUID format' })
@IsNotEmpty()
spaceUuid: string;
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { OccupancyController } from './controllers';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from './services';
@Module({
imports: [ConfigModule],
controllers: [OccupancyController],
providers: [OccupancyService, SqlLoaderService],
})
export class OccupancyModule {}

View File

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

View File

@ -0,0 +1,69 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { GetOccupancyHeatMapBySpaceDto } from '../dto/get-occupancy.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { SpaceParamsDto } from '../dto/occupancy-params.dto';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
@Injectable()
export class OccupancyService {
constructor(
private readonly sqlLoader: SqlLoaderService,
private readonly dataSource: DataSource,
) {}
async getOccupancyHeatMapDataBySpace(
params: SpaceParamsDto,
query: GetOccupancyHeatMapBySpaceDto,
): Promise<BaseResponseDto> {
const { year } = query;
const { spaceUuid } = params;
try {
const data = await this.executeProcedure(
'proceduce_select_fact_space_occupancy',
[spaceUuid, year],
);
const formattedData = data.map((item) => ({
...item,
event_date: new Date(item.event_date).toLocaleDateString('en-CA'), // YYYY-MM-DD
}));
return this.buildResponse(
`Occupancy heat map data fetched successfully for ${spaceUuid ? 'space' : 'community'}`,
formattedData,
);
} catch (error) {
console.error('Failed to fetch occupancy heat map data', {
error,
spaceUuid,
});
throw new HttpException(
error.response?.message || 'Failed to fetch occupancy heat map data',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private buildResponse(message: string, data: any[]) {
return new SuccessResponseDto({
message,
data,
statusCode: HttpStatus.OK,
});
}
private async executeProcedure(
procedureFileName: string,
params: (string | number | null)[],
): Promise<any[]> {
const query = this.loadQuery(
'fact_space_occupancy_count',
procedureFileName,
);
return await this.dataSource.query(query, params);
}
private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
}
}

View File

@ -60,6 +60,7 @@ import { SubspaceProductAllocationService } from 'src/space/services/subspace/su
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
import { SpaceModelProductAllocationService } from 'src/space-model/services/space-model-product-allocation.service'; import { SpaceModelProductAllocationService } from 'src/space-model/services/space-model-product-allocation.service';
import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service'; import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Module({ @Module({
imports: [ConfigModule], imports: [ConfigModule],
controllers: [PowerClampController], controllers: [PowerClampController],
@ -109,6 +110,7 @@ import { SubspaceModelProductAllocationService } from 'src/space-model/services/
SubspaceModelProductAllocationService, SubspaceModelProductAllocationService,
SpaceModelProductAllocationRepoitory, SpaceModelProductAllocationRepoitory,
SubspaceModelProductAllocationRepoitory, SubspaceModelProductAllocationRepoitory,
OccupancyService,
], ],
exports: [PowerClamp], exports: [PowerClamp],
}) })

View File

@ -67,6 +67,7 @@ import {
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
const CommandHandlers = [CreateOrphanSpaceHandler]; const CommandHandlers = [CreateOrphanSpaceHandler];
@ -124,6 +125,7 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
exports: [ProjectService, CqrsModule], exports: [ProjectService, CqrsModule],
}) })

View File

@ -65,6 +65,7 @@ import {
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
const CommandHandlers = [ const CommandHandlers = [
PropogateUpdateSpaceModelHandler, PropogateUpdateSpaceModelHandler,
@ -124,6 +125,7 @@ const CommandHandlers = [
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
exports: [CqrsModule, SpaceModelService], exports: [CqrsModule, SpaceModelService],
}) })

View File

@ -90,6 +90,7 @@ import {
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
export const CommandHandlers = [DisableSpaceHandler]; export const CommandHandlers = [DisableSpaceHandler];
@ -166,6 +167,7 @@ export const CommandHandlers = [DisableSpaceHandler];
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
PowerClampService, PowerClampService,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
exports: [SpaceService], exports: [SpaceService],
}) })

View File

@ -29,6 +29,7 @@ import {
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories'; } from '@app/common/modules/power-clamp/repositories';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { OccupancyService } from '@app/common/helper/services/occupancy.service';
@Module({ @Module({
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
controllers: [VisitorPasswordController], controllers: [VisitorPasswordController],
@ -55,6 +56,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service
PowerClampDailyRepository, PowerClampDailyRepository,
PowerClampMonthlyRepository, PowerClampMonthlyRepository,
SqlLoaderService, SqlLoaderService,
OccupancyService,
], ],
exports: [VisitorPasswordService], exports: [VisitorPasswordService],
}) })