diff --git a/apps/triggers/src/phases/dto/get.phase.dto.ts b/apps/triggers/src/phases/dto/get.phase.dto.ts index f54a2ae..18e54ee 100644 --- a/apps/triggers/src/phases/dto/get.phase.dto.ts +++ b/apps/triggers/src/phases/dto/get.phase.dto.ts @@ -42,7 +42,6 @@ export class GetPhaseByName { example: Phases.PREPAREDNESS, }) @IsEnum(Phases) - @IsNotEmpty() @IsOptional() phase?: Phases; diff --git a/apps/triggers/src/phases/phases.service.ts b/apps/triggers/src/phases/phases.service.ts index 723d479..a6a3691 100644 --- a/apps/triggers/src/phases/phases.service.ts +++ b/apps/triggers/src/phases/phases.service.ts @@ -744,7 +744,7 @@ export class PhasesService { for (const trigger of phase.Trigger) { const { repeatKey } = trigger; if (trigger.source === DataSource.MANUAL) { - await this.triggerService.create( + await this.triggerService.createTrigger( appId, { title: trigger.title, @@ -756,7 +756,7 @@ export class PhasesService { trigger.createdBy, ); } else { - await this.triggerService.create( + await this.triggerService.createTrigger( appId, { title: trigger.title, diff --git a/apps/triggers/src/sources-data/data-source-events.listener.ts b/apps/triggers/src/sources-data/data-source-events.listener.ts index b9d035a..88f5f0b 100644 --- a/apps/triggers/src/sources-data/data-source-events.listener.ts +++ b/apps/triggers/src/sources-data/data-source-events.listener.ts @@ -245,7 +245,7 @@ export class DataSourceEventsListener { if (meetsThreshold) { this.logger.log(`Trigger ${trigger.id} MET threshold`); // update trigger - await this.triggerService.activateTrigger(trigger.uuid, '', trigger); + // await this.triggerService.activateTrigger(trigger.uuid, '', trigger); } else { this.logger.log(`Trigger ${trigger.id} NOT met`); } diff --git a/apps/triggers/src/trigger/dto/activate-trigger-payload.dto.ts b/apps/triggers/src/trigger/dto/activate-trigger-payload.dto.ts new file mode 100644 index 0000000..a4043a5 --- /dev/null +++ b/apps/triggers/src/trigger/dto/activate-trigger-payload.dto.ts @@ -0,0 +1,52 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsOptional, IsArray } from 'class-validator'; + +export class ActivateTriggerPayloadDto { + @ApiProperty({ + description: 'Trigger UUID', + required: false, + }) + @IsOptional() + @IsString() + uuid?: string; + + @ApiProperty({ + description: 'Trigger repeat key', + required: false, + }) + @IsOptional() + @IsString() + repeatKey?: string; + + @ApiProperty({ + description: 'Application ID', + required: false, + }) + @IsOptional() + @IsString() + appId?: string; + + @ApiProperty({ + description: 'Trigger documents', + required: false, + }) + @IsOptional() + @IsArray() + triggerDocuments?: unknown[]; + + @ApiProperty({ + description: 'User information', + required: false, + }) + @IsOptional() + user?: { name?: string }; + + @ApiProperty({ + description: 'Notes', + required: false, + }) + @IsOptional() + @IsString() + notes?: string; +} + diff --git a/apps/triggers/src/trigger/dto/create-trigger.dto.ts b/apps/triggers/src/trigger/dto/create-trigger.dto.ts index 5a44de4..3a6d470 100644 --- a/apps/triggers/src/trigger/dto/create-trigger.dto.ts +++ b/apps/triggers/src/trigger/dto/create-trigger.dto.ts @@ -139,13 +139,29 @@ export class CreateTriggerDto { riverBasin?: string; } -export class BulkCreateTriggerDto { +export class CreateTriggerPayloadDto { @ApiProperty({ + description: 'User information', + required: false, + }) + @IsOptional() + user?: { name?: string }; + + @ApiProperty({ + description: 'Application ID', + example: 'app-123', + }) + @IsString() + appId: string; + + @ApiProperty({ + description: 'Array of triggers for bulk create', type: [CreateTriggerDto], - description: 'An array of triggers to be created', + required: false, }) + @IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => CreateTriggerDto) - triggers: CreateTriggerDto[]; + triggers?: CreateTriggerDto[]; } diff --git a/apps/triggers/src/trigger/dto/get-by-location-payload.dto.ts b/apps/triggers/src/trigger/dto/get-by-location-payload.dto.ts new file mode 100644 index 0000000..e7f94a5 --- /dev/null +++ b/apps/triggers/src/trigger/dto/get-by-location-payload.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsOptional, IsNumber } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class GetByLocationPayloadDto { + @ApiProperty({ + description: 'River basin name', + required: false, + }) + @IsOptional() + @IsString() + riverBasin?: string; + + @ApiProperty({ + description: 'Page number', + required: false, + }) + @IsOptional() + @Type(() => Number) + @IsNumber() + page?: number; + + @ApiProperty({ + description: 'Items per page', + required: false, + }) + @IsOptional() + @Type(() => Number) + @IsNumber() + perPage?: number; +} + diff --git a/apps/triggers/src/trigger/dto/get-triggers.dto.ts b/apps/triggers/src/trigger/dto/get-triggers.dto.ts index 4b5fcb9..640b5a8 100644 --- a/apps/triggers/src/trigger/dto/get-triggers.dto.ts +++ b/apps/triggers/src/trigger/dto/get-triggers.dto.ts @@ -2,7 +2,17 @@ import { PartialType } from '@nestjs/swagger'; import { PaginationDto } from 'src/common/dto'; import { ApiProperty } from '@nestjs/swagger'; import { DataSource } from '@lib/database'; -import { IsEnum, IsOptional, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class findOneTriggerDto { + @ApiProperty({ + example: 'trigger-id', + description: 'The UUID of the trigger to retrieve', + }) + @IsString() + @IsNotEmpty() + uuid: string; +} export class GetTriggersDto extends PartialType(PaginationDto) { @ApiProperty({ diff --git a/apps/triggers/src/trigger/dto/index.ts b/apps/triggers/src/trigger/dto/index.ts index bde2fa1..8ddc5f4 100644 --- a/apps/triggers/src/trigger/dto/index.ts +++ b/apps/triggers/src/trigger/dto/index.ts @@ -1,3 +1,5 @@ export * from './create-trigger.dto'; export * from './update-trigger.dto'; export * from './get-triggers.dto'; +export * from './activate-trigger-payload.dto'; +export * from './get-by-location-payload.dto'; diff --git a/apps/triggers/src/trigger/dto/update-trigger.dto.ts b/apps/triggers/src/trigger/dto/update-trigger.dto.ts index 8ff4c60..8700c6f 100644 --- a/apps/triggers/src/trigger/dto/update-trigger.dto.ts +++ b/apps/triggers/src/trigger/dto/update-trigger.dto.ts @@ -1,9 +1,32 @@ -import { PartialType } from '@nestjs/swagger'; +import { ApiProperty, PartialType } from '@nestjs/swagger'; import { CreateTriggerDto } from './create-trigger.dto'; +import { IsString } from 'class-validator'; -export class UpdateTriggerDto extends PartialType(CreateTriggerDto) {} +export class UpdateTriggerPayloadDto extends PartialType(CreateTriggerDto) { + @ApiProperty({ + description: 'Application ID', + }) + @IsString() + appId: string; +} export class UpdateTriggerTransactionDto { + @ApiProperty({ + description: 'Trigger UUID', + }) + @IsString() uuid: string; + + @ApiProperty({ + description: 'Transaction Hash', + }) transactionHash: string; } + +export class RemoveTriggerPayloadDto { + @ApiProperty({ + description: 'Trigger UUID', + }) + @IsString() + uuid: string; +} diff --git a/apps/triggers/src/trigger/trigger.constants.ts b/apps/triggers/src/trigger/trigger.constants.ts new file mode 100644 index 0000000..640e854 --- /dev/null +++ b/apps/triggers/src/trigger/trigger.constants.ts @@ -0,0 +1,4 @@ +export const TRIGGER_CONSTANTS = { + MICROSERVICE_TIMEOUT_SHORT_MS: 3000, + MICROSERVICE_TIMEOUT_LONG_MS: 30000, +} as const; diff --git a/apps/triggers/src/trigger/trigger.controller.spec.ts b/apps/triggers/src/trigger/trigger.controller.spec.ts index cb280a5..bedb6ca 100644 --- a/apps/triggers/src/trigger/trigger.controller.spec.ts +++ b/apps/triggers/src/trigger/trigger.controller.spec.ts @@ -4,7 +4,11 @@ import { TriggerController } from './trigger.controller'; import { TriggerService } from './trigger.service'; import { PhasesService } from 'src/phases/phases.service'; import { CORE_MODULE } from 'src/constant'; -import { GetTriggersDto, UpdateTriggerTransactionDto } from './dto'; +import { + GetTriggersDto, + UpdateTriggerTransactionDto, + GetByLocationPayloadDto, +} from './dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; describe('TriggerController', () => { @@ -115,37 +119,39 @@ describe('TriggerController', () => { }); describe('create', () => { - it('should create single trigger', async () => { + it('should create triggers with payload', async () => { const mockPayload = { user: { name: 'test-user' }, appId: 'app-id', - title: 'Test Trigger', - description: 'Test Description', - source: DataSource.MANUAL, + triggers: [ + { + title: 'Test Trigger', + description: 'Test Description', + source: DataSource.MANUAL, + }, + ], }; - const mockCreatedTrigger = { - uuid: 'trigger-uuid', - title: 'Test Trigger', - description: 'Test Description', - }; + const mockCreatedTriggers = [ + { + uuid: 'trigger-uuid', + title: 'Test Trigger', + description: 'Test Description', + phase: { + name: 'Test Phase', + riverBasin: 'Test Basin', + }, + }, + ]; jest .spyOn(mockTriggerService, 'create') - .mockResolvedValue(mockCreatedTrigger); + .mockResolvedValue(mockCreatedTriggers as any); const result = await controller.create(mockPayload); - expect(mockTriggerService.create).toHaveBeenCalledWith( - 'app-id', - { - title: 'Test Trigger', - description: 'Test Description', - source: DataSource.MANUAL, - }, - 'test-user', - ); - expect(result).toEqual(mockCreatedTrigger); + expect(mockTriggerService.create).toHaveBeenCalledWith(mockPayload); + expect(result).toEqual(mockCreatedTriggers); }); it('should create bulk triggers', async () => { @@ -165,21 +171,31 @@ describe('TriggerController', () => { }; const mockCreatedTriggers = [ - { uuid: 'trigger-1', title: 'Trigger 1' }, - { uuid: 'trigger-2', title: 'Trigger 2' }, + { + uuid: 'trigger-1', + title: 'Trigger 1', + phase: { + name: 'Test Phase', + riverBasin: 'Test Basin', + }, + }, + { + uuid: 'trigger-2', + title: 'Trigger 2', + phase: { + name: 'Test Phase', + riverBasin: 'Test Basin', + }, + }, ]; jest - .spyOn(mockTriggerService, 'bulkCreate') - .mockResolvedValue(mockCreatedTriggers); + .spyOn(mockTriggerService, 'create') + .mockResolvedValue(mockCreatedTriggers as any); const result = await controller.create(mockPayload); - expect(mockTriggerService.bulkCreate).toHaveBeenCalledWith( - 'app-id', - mockPayload.triggers, - 'test-user', - ); + expect(mockTriggerService.create).toHaveBeenCalledWith(mockPayload); expect(result).toEqual(mockCreatedTriggers); }); @@ -187,27 +203,33 @@ describe('TriggerController', () => { const mockPayload = { user: { id: 'user-id', name: 'test-user' }, appId: 'app-id', - title: 'Test Trigger', - source: DataSource.MANUAL, + triggers: [ + { + title: 'Test Trigger', + source: DataSource.MANUAL, + }, + ], }; - const mockCreatedTrigger = { - uuid: 'trigger-uuid', - title: 'Test Trigger', - }; + const mockCreatedTriggers = [ + { + uuid: 'trigger-uuid', + title: 'Test Trigger', + phase: { + name: 'Test Phase', + riverBasin: 'Test Basin', + }, + }, + ]; jest .spyOn(mockTriggerService, 'create') - .mockResolvedValue(mockCreatedTrigger); + .mockResolvedValue(mockCreatedTriggers as any); const result = await controller.create(mockPayload); - expect(mockTriggerService.create).toHaveBeenCalledWith( - 'app-id', - { title: 'Test Trigger', source: DataSource.MANUAL }, - 'test-user', - ); - expect(result).toEqual(mockCreatedTrigger); + expect(mockTriggerService.create).toHaveBeenCalledWith(mockPayload); + expect(result).toEqual(mockCreatedTriggers); }); }); @@ -273,12 +295,12 @@ describe('TriggerController', () => { }; jest - .spyOn(mockTriggerService, 'getOne') + .spyOn(mockTriggerService, 'findOne') .mockResolvedValue(mockTrigger as any); const result = await controller.getOne(mockPayload); - expect(mockTriggerService.getOne).toHaveBeenCalledWith(mockPayload); + expect(mockTriggerService.findOne).toHaveBeenCalledWith(mockPayload); expect(result).toEqual(mockTrigger); }); @@ -299,12 +321,12 @@ describe('TriggerController', () => { }; jest - .spyOn(mockTriggerService, 'getOne') + .spyOn(mockTriggerService, 'findOne') .mockResolvedValue(mockTriggerWithPhase as any); const result = await controller.getOne(mockPayloadWithExtra); - expect(mockTriggerService.getOne).toHaveBeenCalledWith( + expect(mockTriggerService.findOne).toHaveBeenCalledWith( mockPayloadWithExtra, ); expect(result).toEqual(mockTriggerWithPhase); @@ -312,15 +334,28 @@ describe('TriggerController', () => { }); describe('getByLocation', () => { - const mockPayload = { - location: 'Test Location', - appId: 'app-id', + const mockPayload: GetByLocationPayloadDto = { + riverBasin: 'Test Basin', + page: 1, + perPage: 10, }; it('should successfully get triggers by location', async () => { const mockTriggers = [ - { uuid: 'trigger-1', title: 'Trigger 1', location: 'Test Location' }, - { uuid: 'trigger-2', title: 'Trigger 2', location: 'Test Location' }, + { + uuid: 'trigger-1', + title: 'Trigger 1', + phase: { + riverBasin: 'Test Basin', + }, + }, + { + uuid: 'trigger-2', + title: 'Trigger 2', + phase: { + riverBasin: 'Test Basin', + }, + }, ]; jest @@ -336,16 +371,19 @@ describe('TriggerController', () => { }); it('should handle getByLocation with different location', async () => { - const mockPayloadDifferentLocation = { - location: 'Different Location', - appId: 'app-id', + const mockPayloadDifferentLocation: GetByLocationPayloadDto = { + riverBasin: 'Different Basin', + page: 1, + perPage: 10, }; const mockDifferentTriggers = [ { uuid: 'trigger-3', title: 'Trigger 3', - location: 'Different Location', + phase: { + riverBasin: 'Different Basin', + }, }, ]; @@ -368,8 +406,8 @@ describe('TriggerController', () => { const mockPayload = { uuid: 'trigger-uuid', appId: 'app-id', - activatedBy: 'test-user', - activatedAt: new Date(), + user: { name: 'test-user' }, + notes: 'Test notes', }; it('should successfully activate trigger', async () => { @@ -387,9 +425,7 @@ describe('TriggerController', () => { const result = await controller.activateTrigger(mockPayload); expect(mockTriggerService.activateTrigger).toHaveBeenCalledWith( - 'trigger-uuid', - 'app-id', - { activatedBy: 'test-user', activatedAt: expect.any(Date) }, + mockPayload, ); expect(result).toEqual(mockActivatedTrigger); }); @@ -398,10 +434,9 @@ describe('TriggerController', () => { const mockPayloadWithExtra = { uuid: 'trigger-uuid', appId: 'app-id', - activatedBy: 'test-user', - activatedAt: new Date(), - reason: 'Test reason', - metadata: { key: 'value' }, + user: { name: 'test-user' }, + notes: 'Test notes', + triggerDocuments: [{ key: 'value' }], }; const mockActivatedTrigger = { @@ -418,14 +453,7 @@ describe('TriggerController', () => { const result = await controller.activateTrigger(mockPayloadWithExtra); expect(mockTriggerService.activateTrigger).toHaveBeenCalledWith( - 'trigger-uuid', - 'app-id', - { - activatedBy: 'test-user', - activatedAt: expect.any(Date), - reason: 'Test reason', - metadata: { key: 'value' }, - }, + mockPayloadWithExtra, ); expect(result).toEqual(mockActivatedTrigger); }); @@ -452,11 +480,7 @@ describe('TriggerController', () => { const result = await controller.updateTrigger(mockPayload); - expect(mockTriggerService.update).toHaveBeenCalledWith( - 'trigger-uuid', - 'app-id', - { title: 'Updated Trigger', description: 'Updated Description' }, - ); + expect(mockTriggerService.update).toHaveBeenCalledWith(mockPayload); expect(result).toEqual(mockUpdatedTrigger); }); @@ -483,9 +507,7 @@ describe('TriggerController', () => { ); expect(mockTriggerService.update).toHaveBeenCalledWith( - 'trigger-uuid', - 'app-id', - { isMandatory: true, notes: 'Updated notes' }, + mockPayloadWithDifferentData, ); expect(result).toEqual(mockUpdatedTrigger); }); @@ -510,8 +532,7 @@ describe('TriggerController', () => { const result = await controller.updateTriggerTransaction(mockPayload); expect(mockTriggerService.updateTransaction).toHaveBeenCalledWith( - 'trigger-uuid', - 'tx-hash-123', + mockPayload, ); expect(result).toEqual(mockUpdatedTrigger); }); @@ -536,8 +557,7 @@ describe('TriggerController', () => { ); expect(mockTriggerService.updateTransaction).toHaveBeenCalledWith( - 'trigger-uuid', - 'different-tx-hash-456', + mockPayloadWithDifferentHash, ); expect(result).toEqual(mockUpdatedTrigger); }); @@ -545,12 +565,12 @@ describe('TriggerController', () => { describe('remove', () => { const mockPayload = { - repeatKey: 'repeat-key-123', + uuid: 'trigger-uuid', }; it('should successfully remove trigger', async () => { const mockRemovedTrigger = { - repeatKey: 'repeat-key-123', + uuid: 'trigger-uuid', isDeleted: true, }; @@ -560,17 +580,17 @@ describe('TriggerController', () => { const result = await controller.remove(mockPayload); - expect(mockTriggerService.remove).toHaveBeenCalledWith('repeat-key-123'); + expect(mockTriggerService.remove).toHaveBeenCalledWith(mockPayload); expect(result).toEqual(mockRemovedTrigger); }); - it('should handle remove with different repeat key', async () => { + it('should handle remove with different uuid', async () => { const mockPayloadWithDifferentKey = { - repeatKey: 'different-repeat-key-456', + uuid: 'different-trigger-uuid', }; const mockRemovedTrigger = { - repeatKey: 'different-repeat-key-456', + uuid: 'different-trigger-uuid', isDeleted: true, }; @@ -581,7 +601,7 @@ describe('TriggerController', () => { const result = await controller.remove(mockPayloadWithDifferentKey); expect(mockTriggerService.remove).toHaveBeenCalledWith( - 'different-repeat-key-456', + mockPayloadWithDifferentKey, ); expect(result).toEqual(mockRemovedTrigger); }); diff --git a/apps/triggers/src/trigger/trigger.controller.ts b/apps/triggers/src/trigger/trigger.controller.ts index c5f1fc6..aca17a2 100644 --- a/apps/triggers/src/trigger/trigger.controller.ts +++ b/apps/triggers/src/trigger/trigger.controller.ts @@ -1,35 +1,29 @@ -import { BadRequestException, Controller } from '@nestjs/common'; +import { Controller, Logger } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { MS_TRIGGERS_JOBS } from 'src/constant'; -import { GetTriggersDto, UpdateTriggerTransactionDto } from './dto'; -import { TriggerService } from './trigger.service'; import { - triggerPayloadSchema, - bulkTriggerPayloadSchema, -} from './validation/trigger.schema'; + GetTriggersDto, + UpdateTriggerTransactionDto, + CreateTriggerPayloadDto, + ActivateTriggerPayloadDto, + UpdateTriggerPayloadDto, + GetByLocationPayloadDto, + RemoveTriggerPayloadDto, + findOneTriggerDto, +} from './dto'; +import { TriggerService } from './trigger.service'; @Controller('trigger') export class TriggerController { + private readonly logger = new Logger(TriggerController.name); + constructor(private readonly triggerService: TriggerService) {} @MessagePattern({ cmd: MS_TRIGGERS_JOBS.TRIGGER.ADD, }) - async create(payload: any) { - // here we are checking if the payload is an array for bulk create - // also we are checking if the payload is an object as it is may be use for single create in others modules - // we are using here at once because we have to use the same method for different moddules that is job schedule - - const { user, appId, ...rest } = payload; - if (Array.isArray(payload?.triggers)) { - return await this.triggerService.bulkCreate( - appId, - payload.triggers, - user?.name, - ); - } - - return await this.triggerService.create(appId, rest, user?.name); + async create(payload: CreateTriggerPayloadDto) { + return this.triggerService.create(payload); } @MessagePattern({ @@ -42,48 +36,42 @@ export class TriggerController { @MessagePattern({ cmd: MS_TRIGGERS_JOBS.TRIGGER.GET_ONE, }) - getOne(payload: any) { - return this.triggerService.getOne(payload); + getOne(payload: findOneTriggerDto) { + return this.triggerService.findOne(payload); } @MessagePattern({ cmd: MS_TRIGGERS_JOBS.TRIGGER.GET_BY_LOCATION, }) - getByLocation(payload): Promise { + getByLocation(payload: GetByLocationPayloadDto): Promise { return this.triggerService.findByLocation(payload); } @MessagePattern({ cmd: MS_TRIGGERS_JOBS.TRIGGER.ACTIVATE, }) - activateTrigger(payload) { - const { uuid, appId, ...dto } = payload; - return this.triggerService.activateTrigger(uuid, appId, dto); + activateTrigger(payload: ActivateTriggerPayloadDto) { + return this.triggerService.activateTrigger(payload); } @MessagePattern({ cmd: MS_TRIGGERS_JOBS.TRIGGER.UPDATE, }) - updateTrigger(payload) { - const { uuid, appId, ...dto } = payload; - return this.triggerService.update(uuid, appId, dto); + updateTrigger(payload: UpdateTriggerPayloadDto) { + return this.triggerService.update(payload); } @MessagePattern({ cmd: MS_TRIGGERS_JOBS.TRIGGER.UPDATE_TRANSCTION, }) updateTriggerTransaction(payload: UpdateTriggerTransactionDto) { - return this.triggerService.updateTransaction( - payload.uuid, - payload.transactionHash, - ); + return this.triggerService.updateTransaction(payload); } @MessagePattern({ cmd: MS_TRIGGERS_JOBS.TRIGGER.REMOVE, }) - remove(payload) { - const { repeatKey } = payload; - return this.triggerService.remove(repeatKey); + remove(payload: RemoveTriggerPayloadDto) { + return this.triggerService.remove(payload); } } diff --git a/apps/triggers/src/trigger/trigger.service.spec.ts b/apps/triggers/src/trigger/trigger.service.spec.ts index f20787a..d5aef0c 100644 --- a/apps/triggers/src/trigger/trigger.service.spec.ts +++ b/apps/triggers/src/trigger/trigger.service.spec.ts @@ -6,7 +6,7 @@ import { of } from 'rxjs'; import { TriggerService } from './trigger.service'; import { PhasesService } from 'src/phases/phases.service'; import { CORE_MODULE, JOBS, EVENTS } from 'src/constant'; -import { CreateTriggerDto, GetTriggersDto, UpdateTriggerDto } from './dto'; +import { GetTriggersDto } from './dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; // Mock the paginator function @@ -22,9 +22,7 @@ describe('TriggerService', () => { let mockPhasesService: jest.Mocked; let eventEmitter: jest.Mocked; - let mockScheduleQueue: jest.Mocked; let mockTriggerQueue: jest.Mocked; - let mockStellarQueue: jest.Mocked; const mockPrismaServiceImplementation = { trigger: { @@ -32,6 +30,7 @@ describe('TriggerService', () => { findUnique: jest.fn(), findMany: jest.fn(), update: jest.fn(), + updateMany: jest.fn(), delete: jest.fn(), count: jest.fn(), findFirst: jest.fn(), @@ -75,6 +74,7 @@ describe('TriggerService', () => { const mockTriggerQueueImplementation = { add: jest.fn(), + addBulk: jest.fn(), process: jest.fn(), getJob: jest.fn(), removeJobs: jest.fn(), @@ -129,9 +129,7 @@ describe('TriggerService', () => { mockClientProxy = module.get(CORE_MODULE); mockPhasesService = module.get(PhasesService); eventEmitter = module.get(EventEmitter2); - mockScheduleQueue = module.get('BullQueue_SCHEDULE'); mockTriggerQueue = module.get('BullQueue_TRIGGER'); - mockStellarQueue = module.get('BullQueue_STELLAR'); }); afterEach(() => { @@ -143,15 +141,21 @@ describe('TriggerService', () => { }); describe('create', () => { - const mockCreateTriggerDto: CreateTriggerDto = { - title: 'Test Trigger', - description: 'Test Description', - triggerStatement: { condition: 'test' }, - phaseId: 'phase-uuid', - isMandatory: true, - source: DataSource.MANUAL, - riverBasin: 'Test Basin', - notes: 'Test Notes', + const mockCreateTriggerPayload = { + user: { name: 'user-name' }, + appId: 'app-id', + triggers: [ + { + title: 'Test Trigger', + description: 'Test Description', + triggerStatement: { condition: 'test' }, + phaseId: 'phase-uuid', + isMandatory: true, + source: DataSource.MANUAL, + riverBasin: 'Test Basin', + notes: 'Test Notes', + }, + ], }; const mockCreatedTrigger = { @@ -180,11 +184,7 @@ describe('TriggerService', () => { mockPrismaService.trigger.create.mockResolvedValue(mockCreatedTrigger); mockClientProxy.send.mockReturnValue(of({ name: 'test-action' })); - const result = await service.create( - 'app-id', - mockCreateTriggerDto, - 'user-name', - ); + const result = await service.create(mockCreateTriggerPayload); expect(mockPrismaService.trigger.create).toHaveBeenCalled(); expect(mockClientProxy.send).toHaveBeenCalledWith( @@ -195,82 +195,29 @@ describe('TriggerService', () => { ]), }), ); - expect(result).toEqual(mockCreatedTrigger); + expect(result).toEqual([mockCreatedTrigger]); }); it('should successfully create a non-manual trigger', async () => { - const nonManualDto = { - ...mockCreateTriggerDto, - source: DataSource.DHM, - }; - - // Mock the schedule queue to return a job with repeat key - const mockJob = { - opts: { - repeat: { - key: 'repeat-key-123', + const nonManualPayload = { + ...mockCreateTriggerPayload, + triggers: [ + { + ...mockCreateTriggerPayload.triggers[0], + source: DataSource.DHM, + triggerStatement: { + source: 'water_level_m', + sourceSubType: 'warning_level', + operator: '>', + value: 10, + expression: 'warning_level > 10', + }, + riverBasin: 'Test Basin', }, - }, + ], }; - mockScheduleQueue.add.mockResolvedValue(mockJob as any); - - mockPrismaService.trigger.create.mockResolvedValue(mockCreatedTrigger); - mockClientProxy.send.mockReturnValue(of({ name: 'test-action' })); - - const result = await service.create('app-id', nonManualDto, 'user-name'); - - expect(mockScheduleQueue.add).toHaveBeenCalled(); - expect(mockClientProxy.send).toHaveBeenCalled(); - expect(result).toEqual(mockCreatedTrigger); - }); - - it('should handle create error', async () => { - const error = new Error('Database error'); - mockPrismaService.trigger.create.mockRejectedValue(error); - - await expect( - service.create('app-id', mockCreateTriggerDto, 'user-name'), - ).rejects.toThrow(RpcException); - }); - }); - - describe('bulkCreate', () => { - const mockTriggers = [ - { - title: 'Trigger 1', - source: DataSource.MANUAL, - phaseId: 'phase-uuid', - }, - { - title: 'Trigger 2', - source: DataSource.DHM, - phaseId: 'phase-uuid', - }, - ]; - it('should successfully create multiple triggers', async () => { - const mockCreatedTriggers = [ - { - uuid: 'trigger-1', - title: 'Trigger 1', - isMandatory: true, - source: DataSource.MANUAL, - phase: { name: 'Test Phase', riverBasin: 'Test Basin' }, - triggerStatement: { condition: 'test' }, - notes: 'Test notes', - }, - { - uuid: 'trigger-2', - title: 'Trigger 2', - isMandatory: false, - source: DataSource.DHM, - phase: { name: 'Test Phase', riverBasin: 'Test Basin' }, - triggerStatement: { condition: 'test' }, - notes: 'Test notes', - }, - ]; - - // Mock phase service for manual triggers + // Mock phase service to return a valid phase mockPhasesService.getOne.mockResolvedValue({ id: 1, uuid: 'phase-uuid', @@ -278,55 +225,40 @@ describe('TriggerService', () => { riverBasin: 'Test Basin', } as any); - // Mock schedule queue for non-manual triggers - const mockJob = { - opts: { - repeat: { - key: 'repeat-key-456', - }, - }, - }; - mockScheduleQueue.add.mockResolvedValue(mockJob as any); - - // Mock trigger creation for both manual and non-manual triggers - mockPrismaService.trigger.create - .mockResolvedValueOnce(mockCreatedTriggers[0]) - .mockResolvedValueOnce(mockCreatedTriggers[1]); - + mockPrismaService.trigger.create.mockResolvedValue(mockCreatedTrigger); mockClientProxy.send.mockReturnValue(of({ name: 'test-action' })); - const result = await service.bulkCreate( - 'app-id', - mockTriggers, - 'user-name', - ); + const result = await service.create(nonManualPayload); - expect(result).toEqual(mockCreatedTriggers); + expect(mockClientProxy.send).toHaveBeenCalled(); + expect(result).toEqual([mockCreatedTrigger]); }); - it('should handle bulk create error', async () => { - const error = new Error('Bulk create error'); + it('should handle create error', async () => { + const error = new Error('Database error'); mockPrismaService.trigger.create.mockRejectedValue(error); - await expect( - service.bulkCreate('app-id', mockTriggers, 'user-name'), - ).rejects.toThrow(RpcException); + await expect(service.create(mockCreateTriggerPayload)).rejects.toThrow( + RpcException, + ); }); }); describe('updateTransaction', () => { - const mockUuid = 'trigger-uuid'; - const mockTransactionHash = 'tx-hash-123'; + const mockPayload = { + uuid: 'trigger-uuid', + transactionHash: 'tx-hash-123', + }; it('should successfully update transaction hash', async () => { const mockExistingTrigger = { - uuid: mockUuid, + uuid: 'trigger-uuid', title: 'Existing Trigger', }; const mockUpdatedTrigger = { - uuid: mockUuid, - transactionHash: mockTransactionHash, + uuid: 'trigger-uuid', + transactionHash: 'tx-hash-123', }; mockPrismaService.trigger.findUnique.mockResolvedValue( @@ -334,17 +266,14 @@ describe('TriggerService', () => { ); mockPrismaService.trigger.update.mockResolvedValue(mockUpdatedTrigger); - const result = await service.updateTransaction( - mockUuid, - mockTransactionHash, - ); + const result = await service.updateTransaction(mockPayload); expect(mockPrismaService.trigger.findUnique).toHaveBeenCalledWith({ - where: { uuid: mockUuid }, + where: { uuid: 'trigger-uuid' }, }); expect(mockPrismaService.trigger.update).toHaveBeenCalledWith({ - where: { uuid: mockUuid }, - data: { transactionHash: mockTransactionHash }, + where: { uuid: 'trigger-uuid' }, + data: { transactionHash: 'tx-hash-123' }, }); expect(result).toEqual(mockUpdatedTrigger); }); @@ -352,23 +281,23 @@ describe('TriggerService', () => { it('should handle trigger not found', async () => { mockPrismaService.trigger.findUnique.mockResolvedValue(null); - await expect( - service.updateTransaction(mockUuid, mockTransactionHash), - ).rejects.toThrow(RpcException); + await expect(service.updateTransaction(mockPayload)).rejects.toThrow( + RpcException, + ); }); }); describe('update', () => { - const mockUuid = 'trigger-uuid'; - const mockAppId = 'app-id'; - const mockUpdateTriggerDto: UpdateTriggerDto = { + const mockUpdateTriggerPayload = { + uuid: 'trigger-uuid', + appId: 'app-id', title: 'Updated Trigger', description: 'Updated Description', }; it('should successfully update a trigger', async () => { const mockExistingTrigger = { - uuid: mockUuid, + uuid: 'trigger-uuid', title: 'Existing Trigger', isTriggered: false, triggerStatement: { condition: 'existing' }, @@ -379,7 +308,7 @@ describe('TriggerService', () => { }; const mockUpdatedTrigger = { - uuid: mockUuid, + uuid: 'trigger-uuid', title: 'Updated Trigger', description: 'Updated Description', triggerStatement: { condition: 'existing' }, @@ -393,17 +322,13 @@ describe('TriggerService', () => { mockPrismaService.trigger.update.mockResolvedValue(mockUpdatedTrigger); mockClientProxy.send.mockReturnValue(of({ name: 'test-action' })); - const result = await service.update( - mockUuid, - mockAppId, - mockUpdateTriggerDto, - ); + const result = await service.update(mockUpdateTriggerPayload); expect(mockPrismaService.trigger.findUnique).toHaveBeenCalledWith({ - where: { uuid: mockUuid }, + where: { uuid: 'trigger-uuid' }, }); expect(mockPrismaService.trigger.update).toHaveBeenCalledWith({ - where: { uuid: mockUuid }, + where: { uuid: 'trigger-uuid' }, data: expect.objectContaining({ title: 'Updated Trigger', description: 'Updated Description', @@ -415,14 +340,14 @@ describe('TriggerService', () => { it('should handle trigger not found', async () => { mockPrismaService.trigger.findUnique.mockResolvedValue(null); - await expect( - service.update(mockUuid, mockAppId, mockUpdateTriggerDto), - ).rejects.toThrow(RpcException); + await expect(service.update(mockUpdateTriggerPayload)).rejects.toThrow( + RpcException, + ); }); it('should handle already triggered trigger', async () => { const mockTriggeredTrigger = { - uuid: mockUuid, + uuid: 'trigger-uuid', isTriggered: true, }; @@ -430,9 +355,9 @@ describe('TriggerService', () => { mockTriggeredTrigger, ); - await expect( - service.update(mockUuid, mockAppId, mockUpdateTriggerDto), - ).rejects.toThrow(RpcException); + await expect(service.update(mockUpdateTriggerPayload)).rejects.toThrow( + RpcException, + ); }); }); @@ -457,8 +382,7 @@ describe('TriggerService', () => { }, }; - // Mock the paginate function directly - const mockPaginate = jest.fn().mockResolvedValue(mockPaginatedResult); + // Mock the getAll method directly jest .spyOn(service as any, 'getAll') .mockImplementation(async () => mockPaginatedResult); @@ -469,24 +393,24 @@ describe('TriggerService', () => { }); }); - describe('getOne', () => { + describe('findOne', () => { const mockPayload = { uuid: 'trigger-uuid', }; - it('should successfully get one trigger', async () => { + it('should successfully find one trigger', async () => { const mockTrigger = { uuid: 'trigger-uuid', title: 'Test Trigger', }; - mockPrismaService.trigger.findFirst.mockResolvedValue(mockTrigger); + mockPrismaService.trigger.findUnique.mockResolvedValue(mockTrigger); - const result = await service.getOne(mockPayload); + const result = await service.findOne(mockPayload); - expect(mockPrismaService.trigger.findFirst).toHaveBeenCalledWith({ + expect(mockPrismaService.trigger.findUnique).toHaveBeenCalledWith({ where: { - OR: [{ uuid: 'trigger-uuid' }, { repeatKey: undefined }], + uuid: 'trigger-uuid', }, include: { phase: { @@ -500,100 +424,14 @@ describe('TriggerService', () => { }); }); - describe('isValidDataSource', () => { - it('should return true for valid data source', () => { - const result = service.isValidDataSource(DataSource.MANUAL); - expect(result).toBe(true); - }); - - it('should return false for invalid data source', () => { - const result = service.isValidDataSource('INVALID' as DataSource); - expect(result).toBe(false); - }); - }); - - describe('createManualTrigger', () => { - const mockCreateTriggerDto: CreateTriggerDto = { - title: 'Manual Trigger', - description: 'Manual Description', - triggerStatement: { condition: 'manual' }, - phaseId: 'phase-uuid', - isMandatory: true, - source: DataSource.MANUAL, - notes: 'Manual Notes', - }; - - it('should successfully create manual trigger', async () => { - const mockManualTrigger = { - uuid: 'manual-trigger-uuid', - title: 'Manual Trigger', - description: 'Manual Description', - triggerStatement: { condition: 'manual' }, - phase: { - name: 'Manual Phase', - riverBasin: 'Manual Basin', - }, - isMandatory: true, - source: DataSource.MANUAL, - notes: 'Manual Notes', - }; - - mockPhasesService.getOne.mockResolvedValue({ - id: 1, - uuid: 'phase-uuid', - name: 'Manual Phase', - riverBasin: 'Manual Basin', - } as any); - - mockPrismaService.trigger.create.mockResolvedValue(mockManualTrigger); - - const result = await service.createManualTrigger( - 'app-id', - mockCreateTriggerDto, - 'user-name', - ); - - expect(mockPhasesService.getOne).toHaveBeenCalledWith('phase-uuid'); - expect(mockPrismaService.trigger.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ - title: 'Manual Trigger', - description: 'Manual Description', - triggerStatement: { condition: 'manual' }, - isMandatory: true, - source: DataSource.MANUAL, - notes: 'Manual Notes', - phase: { - connect: { - uuid: 'phase-uuid', - }, - }, - }), - include: { - phase: true, - }, - }); - expect(result).toEqual(mockManualTrigger); - }); - - it('should handle phase not found', async () => { - mockPhasesService.getOne.mockResolvedValue(null); - - await expect( - service.createManualTrigger( - 'app-id', - mockCreateTriggerDto, - 'user-name', - ), - ).rejects.toThrow(RpcException); - }); - }); - describe('remove', () => { - const mockRepeatKey = 'repeat-key-123'; + const mockRemovePayload = { + uuid: 'trigger-uuid', + }; it('should successfully remove trigger', async () => { const mockTrigger = { - repeatKey: mockRepeatKey, + uuid: 'trigger-uuid', isDeleted: false, isTriggered: false, isMandatory: true, @@ -613,30 +451,26 @@ describe('TriggerService', () => { } as any; const mockRemovedTrigger = { - repeatKey: mockRepeatKey, + uuid: 'trigger-uuid', isDeleted: true, }; mockPrismaService.trigger.findUnique.mockResolvedValue(mockTrigger); mockPhasesService.getOne.mockResolvedValue(mockPhaseDetail); - mockScheduleQueue.removeRepeatableByKey.mockResolvedValue(undefined); mockPrismaService.trigger.update.mockResolvedValue(mockRemovedTrigger); mockPrismaService.phase.update.mockResolvedValue({}); - const result = await service.remove(mockRepeatKey); + const result = await service.remove(mockRemovePayload); expect(mockPrismaService.trigger.findUnique).toHaveBeenCalledWith({ where: { - repeatKey: mockRepeatKey, + uuid: 'trigger-uuid', isDeleted: false, }, include: { phase: true }, }); - expect(mockScheduleQueue.removeRepeatableByKey).toHaveBeenCalledWith( - mockRepeatKey, - ); expect(mockPrismaService.trigger.update).toHaveBeenCalledWith({ - where: { repeatKey: mockRepeatKey }, + where: { uuid: 'trigger-uuid' }, data: { isDeleted: true }, }); expect(result).toEqual(mockRemovedTrigger); @@ -645,12 +479,14 @@ describe('TriggerService', () => { it('should handle trigger not found', async () => { mockPrismaService.trigger.findUnique.mockResolvedValue(null); - await expect(service.remove(mockRepeatKey)).rejects.toThrow(RpcException); + await expect(service.remove(mockRemovePayload)).rejects.toThrow( + RpcException, + ); }); it('should handle already triggered trigger', async () => { const mockTriggeredTrigger = { - repeatKey: mockRepeatKey, + uuid: 'trigger-uuid', isDeleted: false, isTriggered: true, }; @@ -659,12 +495,14 @@ describe('TriggerService', () => { mockTriggeredTrigger, ); - await expect(service.remove(mockRepeatKey)).rejects.toThrow(RpcException); + await expect(service.remove(mockRemovePayload)).rejects.toThrow( + RpcException, + ); }); it('should throw error when trigger belongs to an active phase', async () => { const mockTrigger = { - repeatKey: mockRepeatKey, + uuid: 'trigger-uuid', isDeleted: false, isTriggered: false, isMandatory: true, @@ -676,141 +514,36 @@ describe('TriggerService', () => { mockPrismaService.trigger.findUnique.mockResolvedValue(mockTrigger); - await expect(service.remove(mockRepeatKey)).rejects.toThrow( + await expect(service.remove(mockRemovePayload)).rejects.toThrow( new RpcException('Cannot remove triggers from an active phase.'), ); expect(mockPrismaService.trigger.findUnique).toHaveBeenCalledWith({ where: { - repeatKey: mockRepeatKey, + uuid: 'trigger-uuid', isDeleted: false, }, include: { phase: true }, }); // Ensure no further calls are made expect(mockPhasesService.getOne).not.toHaveBeenCalled(); - expect(mockScheduleQueue.removeRepeatableByKey).not.toHaveBeenCalled(); expect(mockPrismaService.trigger.update).not.toHaveBeenCalled(); }); - - it('should add job to trigger queue on successful removal', async () => { - const mockTrigger = { - repeatKey: mockRepeatKey, - isDeleted: false, - isTriggered: false, - isMandatory: true, - phaseId: 'phase-uuid', - phase: { - isActive: false, - }, - }; - - const mockPhaseDetail: any = { - triggerRequirements: { - optionalTriggers: { - totalTriggers: 5, - }, - }, - requiredOptionalTriggers: 3, - }; - - const mockRemovedTrigger = { - repeatKey: mockRepeatKey, - isDeleted: true, - }; - - mockPrismaService.trigger.findUnique.mockResolvedValue(mockTrigger); - mockPhasesService.getOne.mockResolvedValue(mockPhaseDetail); - mockScheduleQueue.removeRepeatableByKey.mockResolvedValue(undefined); - mockPrismaService.trigger.update.mockResolvedValue(mockRemovedTrigger); - mockTriggerQueue.add.mockResolvedValue(undefined); // Mock triggerQueue.add - - const result = await service.remove(mockRepeatKey); - - expect(mockTriggerQueue.add).toHaveBeenCalledWith( - JOBS.TRIGGER.REACHED_THRESHOLD, - mockTrigger, - { - attempts: 3, - removeOnComplete: true, - backoff: { - type: 'exponential', - delay: 1000, - }, - }, - ); - expect(result).toEqual(mockRemovedTrigger); - }); - }); - - describe('scheduleJob', () => { - const mockPayload = { - title: 'Scheduled Trigger', - description: 'Scheduled Description', - triggerStatement: { condition: 'scheduled' }, - phaseId: 'phase-uuid', - isMandatory: false, - dataSource: DataSource.DHM, - riverBasin: 'Scheduled Basin', - repeatEvery: '30000', - notes: 'Scheduled Notes', - createdBy: 'user-name', - }; - - it('should successfully schedule job', async () => { - const mockScheduledTrigger = { - uuid: 'scheduled-trigger-uuid', - title: 'Scheduled Trigger', - description: 'Scheduled Description', - triggerStatement: { condition: 'scheduled' }, - phase: { - name: 'Scheduled Phase', - riverBasin: 'Scheduled Basin', - }, - isMandatory: false, - source: DataSource.DHM, - notes: 'Scheduled Notes', - }; - - const mockJob = { - opts: { - repeat: { - key: 'repeat-key-789', - }, - }, - }; - - mockScheduleQueue.add.mockResolvedValue(mockJob as any); - mockPrismaService.trigger.create.mockResolvedValue(mockScheduledTrigger); - - const result = await service['scheduleJob'](mockPayload); - - expect(mockScheduleQueue.add).toHaveBeenCalledWith( - JOBS.SCHEDULE.ADD, - expect.objectContaining({ - title: 'Scheduled Trigger', - description: 'Scheduled Description', - }), - expect.any(Object), - ); - expect(result).toEqual(mockScheduledTrigger); - }); }); describe('activateTrigger', () => { - const mockUuid = 'trigger-uuid'; - const mockAppId = 'app-id'; - const mockPayload = { - triggeredBy: 'user-name', - activatedAt: new Date(), + const mockActivatePayload = { + uuid: 'trigger-uuid', + appId: 'app-id', user: { name: 'user-name' }, + notes: 'Test notes', }; it('should successfully activate trigger', async () => { const uuid = 'test-uuid'; const mockTrigger = { - uuid: mockUuid, + uuid: 'trigger-uuid', isTriggered: false, source: DataSource.MANUAL, isMandatory: true, @@ -820,7 +553,7 @@ describe('TriggerService', () => { }; const mockActivatedTrigger = { - uuid: mockUuid, + uuid: 'trigger-uuid', isTriggered: true, triggeredBy: 'user-name', triggeredAt: new Date(), @@ -840,14 +573,10 @@ describe('TriggerService', () => { mockPrismaService.activity.findFirst.mockResolvedValue({ app: 'app-id' }); mockClientProxy.send.mockReturnValue(of({ name: 'test-action' })); - const result = await service.activateTrigger( - mockUuid, - mockAppId, - mockPayload, - ); + const result = await service.activateTrigger(mockActivatePayload); expect(mockPrismaService.trigger.findUnique).toHaveBeenCalledWith({ - where: { uuid: mockUuid }, + where: { uuid: 'trigger-uuid' }, include: { phase: { include: { @@ -877,13 +606,13 @@ describe('TriggerService', () => { mockPrismaService.trigger.findUnique.mockResolvedValue(null); await expect( - service.activateTrigger(mockUuid, mockAppId, mockPayload), + service.activateTrigger(mockActivatePayload), ).rejects.toThrow(RpcException); }); it('should handle already triggered trigger', async () => { const mockTriggeredTrigger = { - uuid: mockUuid, + uuid: 'trigger-uuid', isTriggered: true, }; @@ -892,13 +621,13 @@ describe('TriggerService', () => { ); await expect( - service.activateTrigger(mockUuid, mockAppId, mockPayload), + service.activateTrigger(mockActivatePayload), ).rejects.toThrow(RpcException); }); it('should handle automated trigger activation', async () => { const mockAutomatedTrigger = { - uuid: mockUuid, + uuid: 'trigger-uuid', isTriggered: false, source: DataSource.DHM, }; @@ -908,7 +637,7 @@ describe('TriggerService', () => { ); await expect( - service.activateTrigger(mockUuid, mockAppId, mockPayload), + service.activateTrigger(mockActivatePayload), ).rejects.toThrow(RpcException); }); }); @@ -928,7 +657,6 @@ describe('TriggerService', () => { }; mockPrismaService.trigger.findUnique.mockResolvedValue(mockTrigger); - mockScheduleQueue.removeRepeatableByKey.mockResolvedValue(undefined); mockPrismaService.trigger.update.mockResolvedValue(mockArchivedTrigger); const result = await service.archive(mockRepeatKey); @@ -939,9 +667,6 @@ describe('TriggerService', () => { isDeleted: false, }, }); - expect(mockScheduleQueue.removeRepeatableByKey).toHaveBeenCalledWith( - mockRepeatKey, - ); expect(mockPrismaService.trigger.update).toHaveBeenCalledWith({ where: { repeatKey: mockRepeatKey }, data: { isDeleted: true }, @@ -992,4 +717,298 @@ describe('TriggerService', () => { expect(result).toEqual(mockPaginatedResult); }); }); + + describe('activeAutomatedTriggers', () => { + const mockTriggerIds = [ + 'trigger-uuid-1', + 'trigger-uuid-2', + 'trigger-uuid-3', + ]; + + it('should successfully activate automated triggers', async () => { + const mockTriggers = [ + { + uuid: 'trigger-uuid-1', + phaseId: 'phase-uuid-1', + isMandatory: true, + source: DataSource.DHM, + isTriggered: false, + isDeleted: false, + }, + { + uuid: 'trigger-uuid-2', + phaseId: 'phase-uuid-1', + isMandatory: false, + source: DataSource.DHM, + isTriggered: false, + isDeleted: false, + }, + { + uuid: 'trigger-uuid-3', + phaseId: 'phase-uuid-2', + isMandatory: true, + source: DataSource.GLOFAS, + isTriggered: false, + isDeleted: false, + }, + ]; + + const mockPhase1 = { + uuid: 'phase-uuid-1', + name: 'Phase 1', + riverBasin: 'Test Basin 1', + activeYear: '2025', + }; + + const mockPhase2 = { + uuid: 'phase-uuid-2', + name: 'Phase 2', + riverBasin: 'Test Basin 2', + activeYear: '2025', + }; + + mockPrismaService.trigger.findMany.mockResolvedValue(mockTriggers as any); + mockPrismaService.trigger.updateMany.mockResolvedValue({ count: 3 }); + mockPrismaService.phase.update + .mockResolvedValueOnce({}) + .mockResolvedValueOnce({}); + mockPrismaService.phase.findUnique + .mockResolvedValueOnce(mockPhase1 as any) + .mockResolvedValueOnce(mockPhase2 as any); + mockTriggerQueue.addBulk.mockResolvedValue(undefined); + + await service.activeAutomatedTriggers(mockTriggerIds); + + expect(mockPrismaService.trigger.findMany).toHaveBeenCalledWith({ + where: { + uuid: { in: mockTriggerIds }, + source: { not: DataSource.MANUAL }, + isTriggered: false, + isDeleted: false, + }, + }); + + expect(mockPrismaService.trigger.updateMany).toHaveBeenCalledWith({ + where: { + uuid: { in: mockTriggerIds }, + source: { not: DataSource.MANUAL }, + isTriggered: false, + isDeleted: false, + }, + data: { + isTriggered: true, + triggeredAt: expect.any(Date), + triggeredBy: 'System', + }, + }); + + expect(mockPrismaService.phase.update).toHaveBeenCalledTimes(2); + expect(mockPrismaService.phase.update).toHaveBeenCalledWith({ + where: { uuid: 'phase-uuid-1' }, + data: { + receivedMandatoryTriggers: { increment: 1 }, + receivedOptionalTriggers: { increment: 1 }, + }, + }); + expect(mockPrismaService.phase.update).toHaveBeenCalledWith({ + where: { uuid: 'phase-uuid-2' }, + data: { + receivedMandatoryTriggers: { increment: 1 }, + receivedOptionalTriggers: { increment: 0 }, + }, + }); + + expect(mockTriggerQueue.addBulk).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + name: JOBS.TRIGGER.REACHED_THRESHOLD, + data: expect.objectContaining({ uuid: 'trigger-uuid-1' }), + opts: expect.objectContaining({ + attempts: 3, + removeOnComplete: true, + }), + }), + ]), + ); + + expect(eventEmitter.emit).toHaveBeenCalledTimes(2); + expect(eventEmitter.emit).toHaveBeenCalledWith( + EVENTS.NOTIFICATION.CREATE, + expect.objectContaining({ + payload: expect.objectContaining({ + title: `Trigger Statement Met for ${mockPhase1.riverBasin}`, + description: `The trigger condition has been met for phase ${mockPhase1.name}, year ${mockPhase1.activeYear}, in the ${mockPhase1.riverBasin} river basin.`, + group: 'Trigger Statement', + notify: true, + }), + }), + ); + }); + + it('should handle when some triggers are not found', async () => { + const mockTriggers = [ + { + uuid: 'trigger-uuid-1', + phaseId: 'phase-uuid-1', + isMandatory: true, + source: DataSource.DHM, + isTriggered: false, + isDeleted: false, + }, + ]; + + const mockPhase = { + uuid: 'phase-uuid-1', + name: 'Phase 1', + riverBasin: 'Test Basin', + activeYear: '2025', + }; + + mockPrismaService.trigger.findMany.mockResolvedValue(mockTriggers as any); + mockPrismaService.trigger.updateMany.mockResolvedValue({ count: 1 }); + mockPrismaService.phase.update.mockResolvedValue({}); + mockPrismaService.phase.findUnique.mockResolvedValue(mockPhase as any); + mockTriggerQueue.addBulk.mockResolvedValue(undefined); + + await service.activeAutomatedTriggers(mockTriggerIds); + + expect(mockPrismaService.trigger.findMany).toHaveBeenCalled(); + expect(mockPrismaService.trigger.updateMany).toHaveBeenCalled(); + expect(mockTriggerQueue.addBulk).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ uuid: 'trigger-uuid-1' }), + }), + ]), + ); + }); + + it('should filter out manual triggers', async () => { + const mockTriggers = [ + { + uuid: 'trigger-uuid-1', + phaseId: 'phase-uuid-1', + isMandatory: true, + source: DataSource.MANUAL, + isTriggered: false, + isDeleted: false, + }, + { + uuid: 'trigger-uuid-2', + phaseId: 'phase-uuid-1', + isMandatory: false, + source: DataSource.DHM, + isTriggered: false, + isDeleted: false, + }, + ]; + + const mockPhase = { + uuid: 'phase-uuid-1', + name: 'Phase 1', + riverBasin: 'Test Basin', + activeYear: '2025', + }; + + mockPrismaService.trigger.findMany.mockResolvedValue([ + mockTriggers[1], + ] as any); + mockPrismaService.trigger.updateMany.mockResolvedValue({ count: 1 }); + mockPrismaService.phase.update.mockResolvedValue({}); + mockPrismaService.phase.findUnique.mockResolvedValue(mockPhase as any); + mockTriggerQueue.addBulk.mockResolvedValue(undefined); + + await service.activeAutomatedTriggers(mockTriggerIds); + + expect(mockPrismaService.trigger.findMany).toHaveBeenCalledWith({ + where: { + uuid: { in: mockTriggerIds }, + source: { not: DataSource.MANUAL }, + isTriggered: false, + isDeleted: false, + }, + }); + + expect(mockPrismaService.trigger.updateMany).toHaveBeenCalled(); + expect(mockTriggerQueue.addBulk).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ uuid: 'trigger-uuid-2' }), + }), + ]), + ); + }); + + it('should filter out already triggered triggers', async () => { + const mockTriggers = [ + { + uuid: 'trigger-uuid-1', + phaseId: 'phase-uuid-1', + isMandatory: true, + source: DataSource.DHM, + isTriggered: true, + isDeleted: false, + }, + { + uuid: 'trigger-uuid-2', + phaseId: 'phase-uuid-1', + isMandatory: false, + source: DataSource.DHM, + isTriggered: false, + isDeleted: false, + }, + ]; + + const mockPhase = { + uuid: 'phase-uuid-1', + name: 'Phase 1', + riverBasin: 'Test Basin', + activeYear: '2025', + }; + + mockPrismaService.trigger.findMany.mockResolvedValue([ + mockTriggers[1], + ] as any); + mockPrismaService.trigger.updateMany.mockResolvedValue({ count: 1 }); + mockPrismaService.phase.update.mockResolvedValue({}); + mockPrismaService.phase.findUnique.mockResolvedValue(mockPhase as any); + mockTriggerQueue.addBulk.mockResolvedValue(undefined); + + await service.activeAutomatedTriggers(mockTriggerIds); + + expect(mockPrismaService.trigger.findMany).toHaveBeenCalledWith({ + where: { + uuid: { in: mockTriggerIds }, + source: { not: DataSource.MANUAL }, + isTriggered: false, + isDeleted: false, + }, + }); + + expect(mockPrismaService.trigger.updateMany).toHaveBeenCalled(); + }); + + it('should handle empty triggers array', async () => { + mockPrismaService.trigger.findMany.mockResolvedValue([]); + mockPrismaService.trigger.updateMany.mockResolvedValue({ count: 0 }); + mockTriggerQueue.addBulk.mockResolvedValue(undefined); + + await service.activeAutomatedTriggers(mockTriggerIds); + + expect(mockPrismaService.trigger.findMany).toHaveBeenCalled(); + expect(mockPrismaService.trigger.updateMany).toHaveBeenCalled(); + expect(mockTriggerQueue.addBulk).toHaveBeenCalledWith([]); + expect(mockPrismaService.phase.update).not.toHaveBeenCalled(); + expect(eventEmitter.emit).not.toHaveBeenCalled(); + }); + + it('should handle errors and throw RpcException', async () => { + const error = new Error('Database error'); + mockPrismaService.trigger.findMany.mockRejectedValue(error); + + await expect( + service.activeAutomatedTriggers(mockTriggerIds), + ).rejects.toThrow(RpcException); + }); + }); }); diff --git a/apps/triggers/src/trigger/trigger.service.ts b/apps/triggers/src/trigger/trigger.service.ts index 6c57e62..a2fb337 100644 --- a/apps/triggers/src/trigger/trigger.service.ts +++ b/apps/triggers/src/trigger/trigger.service.ts @@ -5,7 +5,16 @@ import { Injectable, Logger, } from '@nestjs/common'; -import { CreateTriggerDto, GetTriggersDto, UpdateTriggerDto } from './dto'; +import { + ActivateTriggerPayloadDto, + CreateTriggerDto, + CreateTriggerPayloadDto, + findOneTriggerDto, + GetTriggersDto, + RemoveTriggerPayloadDto, + UpdateTriggerPayloadDto, + UpdateTriggerTransactionDto, +} from './dto'; import { paginator, PaginatorTypes, @@ -23,7 +32,7 @@ import { AddTriggerJobDto, UpdateTriggerParamsJobDto } from 'src/common/dto'; import { catchError, lastValueFrom, of, timeout } from 'rxjs'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { triggerPayloadSchema } from './validation/trigger.schema'; -import { tryCatch } from '@lib/core'; +import { TRIGGER_CONSTANTS } from './trigger.constants'; const paginate: PaginatorTypes.PaginateFunction = paginator({ perPage: 10 }); @@ -40,178 +49,66 @@ export class TriggerService { private eventEmitter: EventEmitter2, ) {} - async create(appId: string, dto: CreateTriggerDto, createdBy: string) { - this.logger.log(`Creating trigger for app: ${appId}`); - try { - /* - We don't need to create a trigger sperately if source is manual, because, - we are creating a trigger in the phase itself. and phase is linked with datasource - */ - - let trigger = null; - - if (dto.source === 'MANUAL') { - this.logger.log( - `User requested MANUAL Trigger, So creating manul trigger`, - ); - delete dto.triggerDocuments?.type; - trigger = await this.createManualTrigger(appId, dto, createdBy); - } else { - const result = triggerPayloadSchema.safeParse(dto); - console.log(result); - if (!result.success) { - throw new BadRequestException({ - message: `Invalid trigger payload: ${JSON.stringify(result.error.flatten())}`, - }); - } + async create(payload: CreateTriggerPayloadDto) { + const { user, appId, triggers } = payload; - const sanitizedPayload = { - title: dto.title, - description: dto.description, - triggerStatement: dto.triggerStatement, - phaseId: dto.phaseId, - isMandatory: dto.isMandatory, - dataSource: dto.source, - riverBasin: dto.riverBasin, - repeatEvery: '30000', - notes: dto.notes, - createdBy, - }; - trigger = await this.scheduleJob(sanitizedPayload); - } + if (!appId) { + throw new BadRequestException('appId is required'); + } - const queueData: AddTriggerJobDto = { - id: trigger.uuid, - trigger_type: trigger.isMandatory ? 'MANDATORY' : 'OPTIONAL', - phase: trigger.phase.name, - title: trigger.title, - description: trigger.description, - source: trigger.source, - river_basin: trigger.phase.riverBasin, - params: JSON.parse(JSON.stringify(trigger.triggerStatement)), - is_mandatory: trigger.isMandatory, - notes: trigger.notes, - }; + try { + const triggersData = await Promise.all( + triggers.map((item) => this.createTriggerItem(appId, item, user?.name)), + ); - const res = await lastValueFrom( - this.client - .send( - { cmd: JOBS.STELLAR.ADD_ONCHAIN_TRIGGER_QUEUE, uuid: appId }, - { triggers: [queueData] }, - ) - .pipe( - timeout(3000), - catchError((error) => { - if (error.name === 'TimeoutError') { - // Handle timeout specifically - this.logger.error( - `Error while adding trigger onChain, action ${JOBS.STELLAR.ADD_ONCHAIN_TRIGGER_QUEUE} for AA ${appId}, timeout in 3 Seconds`, - ); - return of(null); - } + const queueData: AddTriggerJobDto[] = triggersData.map((trigger) => + this.buildAddTriggerJobDto(trigger), + ); - this.logger.error( - `Error while adding trigger onChain. Action ${JOBS.STELLAR.ADD_ONCHAIN_TRIGGER_QUEUE} for AA ${appId}, error: ${error.message}`, - ); + const res = await this.sendAddTriggerToOnChain(appId, queueData); - return of(null); - }), - ), - ); - if (!res) { - this.logger.warn(`Trigger Statement onChain not added for AA ${appId}`); - } else { - this.logger.log(` - Trigger added to stellar queue action: ${res?.name} with id: ${queueData.id} for AA ${appId} + this.logger.log(` + Total ${triggersData.length} triggers added for action: ${res?.name} to stellar queue for AA ${appId} `); - } - - return trigger; + return triggersData; } catch (error: any) { - console.log(error); - this.logger.error(error); + this.logger.error(`Error in create triggers for app ${appId}:`, error); throw new RpcException(error.message); } } - async bulkCreate(appId: string, payload, createdBy: string) { - try { - const k = await Promise.all( - payload.map(async (item) => { - if (item.source === 'MANUAL') { - this.logger.log( - `User requested MANUAL Trigger, So creating manul trigger`, - ); - return await this.createManualTrigger(appId, item, createdBy); - } - - const sanitizedPayload = { - title: item.title, - triggerStatement: item.triggerStatement, - phaseId: item.phaseId, - isMandatory: item.isMandatory, - source: item.source, - riverBasin: item.riverBasin, - repeatEvery: '30000', - notes: item.notes, - createdBy, - description: item.description, - }; - - return await this.scheduleJob(sanitizedPayload); - }), + private async createTriggerItem( + appId: string, + dto: CreateTriggerDto, + createdBy: string, + ) { + if (dto.source === DataSource.MANUAL) { + this.logger.log( + `User requested MANUAL Trigger, So creating manual trigger`, ); - const queueData: AddTriggerJobDto[] = k.map((trigger) => { - return { - id: trigger.uuid, - trigger_type: trigger.isMandatory ? 'MANDATORY' : 'OPTIONAL', - phase: trigger.phase.name, - title: trigger.title, - description: trigger.description, - source: trigger.source, - river_basin: trigger.phase.riverBasin, - params: JSON.parse(JSON.stringify(trigger.triggerStatement)), - is_mandatory: trigger.isMandatory, - notes: trigger.notes, - }; - }); + // const dtoCopy = { ...dto }; + // delete dtoCopy.triggerDocuments?.type; + return await this.createTrigger(appId, dto, createdBy); + } - const res = await lastValueFrom( - this.client - .send( - { cmd: JOBS.STELLAR.ADD_ONCHAIN_TRIGGER_QUEUE, uuid: appId }, - { triggers: queueData }, - ) - .pipe( - timeout(30000), - catchError((error) => { - this.logger.error( - `Microservice call failed for add trigger onChain:`, - error, - ); - throw error; - }), - ), - ).catch((error) => { - this.logger.error( - `Microservice call failed for add trigger onChain queue:`, - error, - ); - throw error; + const result = triggerPayloadSchema.safeParse(dto); + if (!result.success) { + this.logger.warn( + `Invalid trigger payload: ${JSON.stringify(result.error.flatten())}`, + ); + throw new BadRequestException({ + message: `Invalid trigger payload: ${JSON.stringify(result.error.flatten())}`, }); - - this.logger.log(` - Total ${k.length} triggers added for action: ${res?.name} to stellar queue for AA ${appId} - `); - return k; - } catch (error: any) { - console.log(error); - throw new RpcException(error.message); } + + return await this.createTrigger(appId, dto, createdBy); } - async updateTransaction(uuid: string, transactionHash: string) { - this.logger.log(`Updating trigger trasaction with uuid: ${uuid}`); + async updateTransaction(payload: UpdateTriggerTransactionDto) { + const { uuid, transactionHash } = payload; + this.logger.log( + `Updating trigger transaction hash on trigger with uuid: ${uuid}`, + ); try { const trigger = await this.prisma.trigger.findUnique({ @@ -236,14 +133,26 @@ export class TriggerService { return updatedTrigger; } catch (error: any) { - this.logger.error(error); + this.logger.error( + `Error in updating trigger transaction hash on trigger with uuid: ${uuid}:`, + error, + ); throw new RpcException(error.message); } } - async update(uuid: string, appId: string, payload: UpdateTriggerDto) { + async update(payload: UpdateTriggerPayloadDto) { + const { uuid, appId, ...dto } = payload; + this.logger.log(`Updating trigger with uuid: ${uuid}`); + if (!uuid) { + throw new BadRequestException('uuid is required'); + } + if (!appId) { + throw new BadRequestException('appId is required'); + } + try { const trigger = await this.prisma.trigger.findUnique({ where: { @@ -258,17 +167,17 @@ export class TriggerService { if (trigger.isTriggered) { this.logger.warn( - 'Trigger has already been activated. Connot update, Activated trigger.', + 'Trigger has already been activated. Cannot update an activated trigger.', ); throw new RpcException('Trigger has already been activated.'); } const fields = { - title: payload.title || trigger.title, - triggerStatement: payload.triggerStatement || trigger.triggerStatement, - notes: payload.notes ?? trigger.notes, - description: payload.description ?? trigger.description, - isMandatory: payload.isMandatory ?? trigger.isMandatory, + title: dto.title || trigger.title, + triggerStatement: dto.triggerStatement || trigger.triggerStatement, + notes: dto.notes ?? trigger.notes, + description: dto.description ?? trigger.description, + isMandatory: dto.isMandatory ?? trigger.isMandatory, }; const updatedTrigger = await this.prisma.trigger.update({ @@ -280,42 +189,9 @@ export class TriggerService { }, }); - // Add job in queue to update trigger onChain hash - const queueData: UpdateTriggerParamsJobDto = { - id: updatedTrigger.uuid, - isTriggered: updatedTrigger.isTriggered, - params: JSON.parse(JSON.stringify(updatedTrigger.triggerStatement)), - source: updatedTrigger.source, - }; + const queueData = this.buildUpdateTriggerParamsJobDto(updatedTrigger); - const res = await lastValueFrom( - this.client - .send( - { - cmd: JOBS.STELLAR.UPDATE_ONCHAIN_TRIGGER_PARAMS_QUEUE, - uuid: appId, - }, - { - trigger: queueData, - }, - ) - .pipe( - timeout(30000), - catchError((error) => { - this.logger.error( - `Microservice call failed for update trigger onChain:`, - error, - ); - throw error; - }), - ), - ).catch((error) => { - this.logger.error( - `Microservice call failed for update trigger onChain queue:`, - error, - ); - throw error; - }); + const res = await this.sendUpdateTriggerToOnChain(appId, queueData); this.logger.log(` Trigger added to stellar queue with id: ${res?.name} for AA ${appId} @@ -374,13 +250,13 @@ export class TriggerService { } } - async getOne(payload: any) { - const { repeatKey, uuid } = payload; - this.logger.log(`Getting trigger with repeatKey: ${repeatKey}`); + async findOne(payload: findOneTriggerDto) { + const { uuid } = payload; + this.logger.log(`Getting trigger with uuid: ${uuid}`); try { - return await this.prisma.trigger.findFirst({ + return await this.prisma.trigger.findUnique({ where: { - OR: [{ uuid: uuid }, { repeatKey: repeatKey }], + uuid, }, include: { phase: { @@ -396,18 +272,8 @@ export class TriggerService { } } - isValidDataSource(value: string): value is DataSource { - return (Object.values(DataSource) as DataSource[]).includes( - value as DataSource, - ); - } - - async createManualTrigger( - appId: string, - dto: CreateTriggerDto, - createdBy: string, - ) { - this.logger.log(`Creating manual trigger for app: ${appId}`); + async createTrigger(appId: string, dto: CreateTriggerDto, createdBy: string) { + this.logger.log(`Creating ${dto.source} trigger for app: ${appId}`); try { const { phaseId, ...rest } = dto; const phase = await this.phasesService.getOne(phaseId); @@ -424,7 +290,7 @@ export class TriggerService { uuid: phaseId, }, }, - source: DataSource.MANUAL, + source: dto.source, isDeleted: false, repeatKey: randomUUID(), createdBy, @@ -444,31 +310,40 @@ export class TriggerService { } } - async remove(repeatKey: string) { - this.logger.log(`Removing trigger with repeatKey: ${repeatKey}`); + async remove(payload: RemoveTriggerPayloadDto) { + const { uuid } = payload; + + this.logger.log(`Removing trigger with uuid: ${uuid}`); + + if (!uuid) { + throw new BadRequestException('Uuid is required'); + } + try { const trigger = await this.prisma.trigger.findUnique({ where: { - repeatKey: repeatKey, + uuid: uuid, isDeleted: false, }, include: { phase: true }, }); if (!trigger) { - this.logger.error(`Active trigger with id: ${repeatKey} not found.`); - throw new RpcException( - `Active trigger with id: ${repeatKey} not found.`, - ); + this.logger.error(`Trigger with id: ${uuid} not found.`); + throw new RpcException(`Trigger with id: ${uuid} not found.`); } if (trigger.isTriggered) { - this.logger.error(`Active trigger with id: ${repeatKey} not found.`); + this.logger.error( + `Trigger with id: ${uuid} is activated. Cannot remove an activated trigger.`, + ); throw new RpcException(`Cannot remove an activated trigger.`); } if (trigger.phase.isActive) { - this.logger.error(`Active trigger with id: ${repeatKey} not found.`); + this.logger.error( + `Trigger with id: ${uuid} is in an active phase. Cannot remove triggers from an active phase.`, + ); throw new RpcException(`Cannot remove triggers from an active phase.`); } @@ -492,25 +367,15 @@ export class TriggerService { // } // } - await this.scheduleQueue.removeRepeatableByKey(repeatKey); const updatedTrigger = await this.prisma.trigger.update({ where: { - repeatKey: repeatKey, + uuid, }, data: { isDeleted: true, }, }); - this.triggerQueue.add(JOBS.TRIGGER.REACHED_THRESHOLD, trigger, { - attempts: 3, - removeOnComplete: true, - backoff: { - type: 'exponential', - delay: 1000, - }, - }); - return updatedTrigger; } catch (error: any) { this.logger.error(error); @@ -518,67 +383,6 @@ export class TriggerService { } } - private async scheduleJob(payload: any) { - this.logger.log( - `Scheduling trigger with payload: ${JSON.stringify(payload)}`, - ); - try { - const uuid = randomUUID(); - const { app, source, ...rest } = payload; - - const jobPayload = { - ...rest, - uuid, - }; - - const repeatable = await this.scheduleQueue.add( - JOBS.SCHEDULE.ADD, - jobPayload, - { - jobId: uuid, - attempts: 3, - removeOnComplete: true, - backoff: { - type: 'exponential', - delay: 1000, - }, - repeat: { - every: Number(payload.repeatEvery), - }, - removeOnFail: true, - }, - ); - const repeatableKey = repeatable.opts.repeat.key; - const { phaseId, ...restJob } = jobPayload; - - const createData = { - ...restJob, - repeatKey: repeatableKey, - phase: { - connect: { - uuid: phaseId, - }, - }, - source, - createdBy: payload.createdBy, - isDeleted: false, - }; - const trigger = await this.prisma.trigger.create({ - data: createData, - include: { - phase: true, - }, - }); - this.logger.log(`Trigger created with repeatKey: ${repeatableKey}`); - - return trigger; - return trigger; - } catch (error: any) { - this.logger.error(error); - throw new RpcException(error.message); - } - } - async activeAutomatedTriggers(ids: string[]) { try { const triggerWhereArgs: Prisma.TriggerWhereInput = { @@ -695,17 +499,23 @@ export class TriggerService { } } - async activateTrigger(uuid: string, appId: string, payload: any) { + async activateTrigger(data: ActivateTriggerPayloadDto) { + const { uuid, appId, ...payload } = data; this.logger.log(`Activating trigger with uuid: ${uuid}`); + if (!uuid) { + throw new BadRequestException('uuid is required'); + } + try { - const { triggeredBy, triggerDocuments, user } = payload; - console.log('payload', payload); + const { triggerDocuments, user } = payload; + this.logger.debug( + `Activating trigger with payload: ${JSON.stringify(payload)}`, + ); const trigger = await this.prisma.trigger.findUnique({ where: { - ...(payload?.repeatKey && { repeatKey: payload?.repeatKey }), - ...(uuid && { uuid: uuid }), + uuid, }, include: { phase: { @@ -726,7 +536,7 @@ export class TriggerService { throw new RpcException('Trigger has already been activated.'); } - if (trigger.source !== DataSource.MANUAL && false) { + if (trigger.source !== DataSource.MANUAL) { this.logger.warn('Cannot activate an automated trigger.'); throw new RpcException('Cannot activate an automated trigger.'); } @@ -751,12 +561,7 @@ export class TriggerService { }, }); - const jobDetails: UpdateTriggerParamsJobDto = { - id: updatedTrigger.uuid, - isTriggered: updatedTrigger.isTriggered, - params: JSON.parse(JSON.stringify(updatedTrigger.triggerStatement)), - source: updatedTrigger.source, - }; + const jobDetails = this.buildUpdateTriggerParamsJobDto(updatedTrigger); if (trigger.isMandatory) { await this.prisma.phase.update({ @@ -813,34 +618,10 @@ export class TriggerService { return updatedTrigger; } - const res = await lastValueFrom( - this.client - .send( - { - cmd: JOBS.STELLAR.UPDATE_ONCHAIN_TRIGGER_PARAMS_QUEUE, - uuid: appId ? appId : appIds?.app, - }, - { - trigger: jobDetails, - }, - ) - .pipe( - timeout(30000), - catchError((error) => { - this.logger.error( - `Microservice call failed for update trigger onChain:`, - error, - ); - throw error; - }), - ), - ).catch((error) => { - this.logger.error( - `Microservice call failed for update trigger onChain queue:`, - error, - ); - throw error; - }); + const res = await this.sendUpdateTriggerToOnChain( + appId ? appId : appIds?.app, + jobDetails, + ); this.logger.log(` Trigger added to stellar queue with id: ${jobDetails.id}, action: ${res?.name} for appId ${appId} @@ -879,7 +660,6 @@ export class TriggerService { ); } - await this.scheduleQueue.removeRepeatableByKey(repeatKey); const updatedTrigger = await this.prisma.trigger.update({ where: { repeatKey: repeatKey, @@ -946,9 +726,6 @@ export class TriggerService { } } - /** - * Find triggers for a specific source and indicator - */ async findTriggersBySourceAndIndicator( source: DataSource, indicator: string, @@ -968,4 +745,99 @@ export class TriggerService { }, }); } + + private buildAddTriggerJobDto(trigger: any): AddTriggerJobDto { + return { + id: trigger.uuid, + trigger_type: trigger.isMandatory ? 'MANDATORY' : 'OPTIONAL', + phase: trigger.phase.name, + title: trigger.title, + description: trigger.description, + source: trigger.source, + river_basin: trigger.phase.riverBasin, + params: JSON.parse(JSON.stringify(trigger.triggerStatement)), + is_mandatory: trigger.isMandatory, + notes: trigger.notes, + }; + } + + private buildUpdateTriggerParamsJobDto( + trigger: any, + ): UpdateTriggerParamsJobDto { + return { + id: trigger.uuid, + isTriggered: trigger.isTriggered, + params: JSON.parse(JSON.stringify(trigger.triggerStatement)), + source: trigger.source, + }; + } + + private async sendAddTriggerToOnChain( + appId: string, + triggers: AddTriggerJobDto[], + ): Promise { + const timeoutMs = + triggers.length > 1 + ? TRIGGER_CONSTANTS.MICROSERVICE_TIMEOUT_LONG_MS + : TRIGGER_CONSTANTS.MICROSERVICE_TIMEOUT_SHORT_MS; + + return lastValueFrom( + this.client + .send( + { cmd: JOBS.STELLAR.ADD_ONCHAIN_TRIGGER_QUEUE, uuid: appId }, + { triggers }, + ) + .pipe( + timeout(timeoutMs), + catchError((error) => { + if (error.name === 'TimeoutError') { + this.logger.error( + `Error while adding trigger onChain, action ${JOBS.STELLAR.ADD_ONCHAIN_TRIGGER_QUEUE} for AA ${appId}, timeout in ${timeoutMs}ms`, + ); + return of(null); + } + + this.logger.error( + `Error while adding trigger onChain. Action ${JOBS.STELLAR.ADD_ONCHAIN_TRIGGER_QUEUE} for AA ${appId}, error: ${error.message}`, + ); + + return of(null); + }), + ), + ); + } + + private async sendUpdateTriggerToOnChain( + appId: string, + trigger: UpdateTriggerParamsJobDto, + ): Promise { + return lastValueFrom( + this.client + .send( + { + cmd: JOBS.STELLAR.UPDATE_ONCHAIN_TRIGGER_PARAMS_QUEUE, + uuid: appId, + }, + { + trigger, + }, + ) + .pipe( + timeout(TRIGGER_CONSTANTS.MICROSERVICE_TIMEOUT_LONG_MS), + catchError((error) => { + this.logger.error( + `Microservice call failed for update trigger onChain:`, + error, + ); + throw error; + }), + ), + ).catch((error) => { + this.logger.error( + `Microservice call failed for update trigger onChain queue:`, + error, + ); + throw error; + }); + } } diff --git a/apps/triggers/src/trigger/validation/trigger.schema.ts b/apps/triggers/src/trigger/validation/trigger.schema.ts index 4f71b0c..2f2e342 100644 --- a/apps/triggers/src/trigger/validation/trigger.schema.ts +++ b/apps/triggers/src/trigger/validation/trigger.schema.ts @@ -124,14 +124,9 @@ export const triggerPayloadSchema = z.object({ isTriggered: z.boolean().optional().default(false), isDeleted: z.boolean().optional().default(false), phaseId: z.string().trim().min(1, 'phaseId is required'), - riverBasin: z.string().trim().min(1, 'riverBasin is required'), + riverBasin: z.string().trim().min(1, 'riverBasin is required').optional(), source: z.nativeEnum(DataSource), }); -export const bulkTriggerPayloadSchema = z - .array(triggerPayloadSchema) - .min(1, 'At least one trigger is required'); - export type TriggerPayload = z.infer; export type TriggerStatement = z.infer; -export type BulkTriggerPayload = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e804d0..820a817 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,7 +179,7 @@ importers: specifier: ^4.2.0 version: 4.2.0 - apps/mock-api: + apps/forcast-api: dependencies: '@lib/core': specifier: workspace:* @@ -296,6 +296,9 @@ importers: cheerio: specifier: ^1.0.0 version: 1.1.2 + expr-eval: + specifier: ^2.0.2 + version: 2.0.2 google-auth-library: specifier: ^9.12.0 version: 9.15.1