From 215e60888e4763565594759347fae44412c39c23 Mon Sep 17 00:00:00 2001 From: Tushar Date: Tue, 22 Jul 2025 16:47:36 +0530 Subject: [PATCH 1/2] Added coderabbit file --- .coderabbit.yaml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..7a4da82 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,35 @@ +language: 'en' + +early_access: false + +reviews: + request_changes_workflow: true + high_level_summary: true + poem: false + review_status: true + collapse_walkthrough: false + path_filters: + - '!**/.xml' + path_instructions: + - path: '**/*.js' + instructions: 'Review the JavaScript code for conformity with the Google JavaScript style guide, highlighting any deviations.' + - path: '**/*.ts' + instructions: | + "Review the JavaScript code for conformity with the Google JavaScript style guide, highlighting any deviations. Ensure that: + - The code adheres to best practices associated with nodejs. + - The code adheres to best practices associated with nestjs framework. + - The code adheres to best practices recommended for performance. + - The code adheres to similar naming conventions for controllers, models, services, methods, variables." + auto_review: + enabled: true + ignore_title_keywords: + - 'WIP' + - 'DO NOT MERGE' + drafts: false + base_branches: + - 'master' + - 'main' + - 'aspire-leaders' + +chat: + auto_reply: true From 528cd0caaaf628a6a99760937b1599340804fb89 Mon Sep 17 00:00:00 2001 From: Tushar Date: Fri, 12 Sep 2025 16:02:32 +0530 Subject: [PATCH 2/2] Update API created --- .../dto/updateNotificationAction.dto.ts | 123 +++++++++ .../notificationActionTemplates.entity.ts | 6 + .../notification_events.controller.ts | 25 ++ .../notification_events.service.ts | 247 +++++++++++++++--- 4 files changed, 371 insertions(+), 30 deletions(-) create mode 100644 src/modules/notification_events/dto/updateNotificationAction.dto.ts diff --git a/src/modules/notification_events/dto/updateNotificationAction.dto.ts b/src/modules/notification_events/dto/updateNotificationAction.dto.ts new file mode 100644 index 0000000..7dd8e9d --- /dev/null +++ b/src/modules/notification_events/dto/updateNotificationAction.dto.ts @@ -0,0 +1,123 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + ValidateNested, +} from "class-validator"; +import { Type } from "class-transformer"; + +export class EmailUpdateDto { + @ApiProperty({ example: "Updated email subject", description: "Email subject" }) + @IsString() + @IsNotEmpty() + @IsOptional() + subject: string; + + @ApiProperty({ example: "Updated email body content", description: "Email body" }) + @IsString() + @IsNotEmpty() + @IsOptional() + body: string; + + @ApiProperty({ example: "Aspire Leaders", description: "Email from name" }) + @IsString() + @IsNotEmpty() + @IsOptional() + emailFromName: string; + + @ApiProperty({ example: "noreply@aspireleaders.org", description: "Email from address" }) + @IsString() + @IsNotEmpty() + @IsOptional() + emailFrom: string; +} + +export class SmsUpdateDto { + @ApiProperty({ example: "Updated SMS subject", description: "SMS subject" }) + @IsString() + @IsNotEmpty() + @IsOptional() + subject: string; + + @ApiProperty({ example: "Updated SMS body content", description: "SMS body" }) + @IsString() + @IsNotEmpty() + @IsOptional() + body: string; +} + +export class PushUpdateDto { + @ApiProperty({ example: "Updated push subject", description: "Push subject" }) + @IsString() + @IsNotEmpty() + @IsOptional() + subject: string; + + @ApiProperty({ example: "Updated push body content", description: "Push body" }) + @IsString() + @IsNotEmpty() + @IsOptional() + body: string; + + @ApiProperty({ example: "https://images.unsplash.com/photo-1519389950473-47ba0277781c", description: "Image URL" }) + @IsString() + @IsOptional() + image: string; + + @ApiProperty({ example: "https://aspireleaders.org/learn", description: "Link URL" }) + @IsString() + @IsOptional() + link: string; +} + +export class UpdateNotificationActionDto { + @ApiProperty({ example: "Updated notification title", description: "Notification title" }) + @IsString() + @IsNotEmpty() + @IsOptional() + title: string; + + @ApiProperty({ example: "published", description: "Status for NotificationActions" }) + @IsEnum(['published', 'unpublished'], { + message: 'Status must be one of: published, unpublished', + }) + @IsString() + @IsNotEmpty() + @IsOptional() + status: string; + + @ApiProperty({ example: "published", description: "Status for NotificationActionTemplates" }) + @IsEnum(['published', 'unpublished'], { + message: 'Template status must be one of: published, unpublished', + }) + @IsString() + @IsNotEmpty() + @IsOptional() + templateStatus: string; + + @ApiProperty({ type: EmailUpdateDto, description: "Email update details" }) + @ValidateNested() + @Type(() => EmailUpdateDto) + @IsOptional() + email?: EmailUpdateDto; + + @ApiProperty({ type: SmsUpdateDto, description: "SMS update details" }) + @ValidateNested() + @Type(() => SmsUpdateDto) + @IsOptional() + sms?: SmsUpdateDto; + + @ApiProperty({ type: PushUpdateDto, description: "Push notification update details" }) + @ValidateNested() + @Type(() => PushUpdateDto) + @IsOptional() + push?: PushUpdateDto; + + @ApiProperty({ example: "uuid-string", description: "Updated by user ID" }) + @IsUUID() + @IsOptional() + updatedBy: string; +} \ No newline at end of file diff --git a/src/modules/notification_events/entity/notificationActionTemplates.entity.ts b/src/modules/notification_events/entity/notificationActionTemplates.entity.ts index 5059ddd..1733b7c 100644 --- a/src/modules/notification_events/entity/notificationActionTemplates.entity.ts +++ b/src/modules/notification_events/entity/notificationActionTemplates.entity.ts @@ -34,6 +34,12 @@ export class NotificationActionTemplates { @Column({ nullable: true }) link: string; + @Column({ nullable: true }) + emailFromName: string; + + @Column({ nullable: true }) + emailFrom: string; + @Column({ type: 'uuid', nullable: true }) updatedBy: string; diff --git a/src/modules/notification_events/notification_events.controller.ts b/src/modules/notification_events/notification_events.controller.ts index a52a8a9..60c98ab 100644 --- a/src/modules/notification_events/notification_events.controller.ts +++ b/src/modules/notification_events/notification_events.controller.ts @@ -6,6 +6,7 @@ import { SearchFilterDto } from './dto/searchTemplateType.dto'; import { Response } from 'express'; import { CreateEventDto } from './dto/createTemplate.dto'; import { UpdateEventDto } from './dto/updateEventTemplate.dto'; +import { UpdateNotificationActionDto } from './dto/updateNotificationAction.dto'; import { AllExceptionsFilter } from 'src/common/filters/exception.filter'; import { APIID } from 'src/common/utils/api-id.config'; import { ERROR_MESSAGES, SUCCESS_MESSAGES } from 'src/common/utils/constant.util'; @@ -75,6 +76,30 @@ export class NotificationEventsController { ); } + @UseFilters(new AllExceptionsFilter(APIID.TEMPLATE_UPDATE)) + @Patch("/action/:id") + @ApiBody({ type: UpdateNotificationActionDto }) + @ApiResponse({ + status: 200, + description: "Notification action updated successfully", + }) + @ApiResponse({ status: 400, description: ERROR_MESSAGES.BAD_REQUEST }) + @ApiResponse({ status: 404, description: ERROR_MESSAGES.TEMPLATE_NOTFOUND }) + @UsePipes(new ValidationPipe({ transform: true })) + updateNotificationAction( + @Param("id") id: number, + @Body() updateNotificationActionDto: UpdateNotificationActionDto, + @Res() response: Response, + @GetUserId() userId: string, + ) { + return this.notificationeventsService.updateNotificationAction( + id, + updateNotificationActionDto, + userId, + response + ); + } + @UseFilters(new AllExceptionsFilter(APIID.TEMPLATE_DELETE)) @Delete("/:id") @UsePipes(new ValidationPipe({ transform: true })) diff --git a/src/modules/notification_events/notification_events.service.ts b/src/modules/notification_events/notification_events.service.ts index 224f66b..fda642b 100644 --- a/src/modules/notification_events/notification_events.service.ts +++ b/src/modules/notification_events/notification_events.service.ts @@ -4,22 +4,23 @@ import { Injectable, InternalServerErrorException, NotFoundException, -} from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; -import { NotificationActions } from "./entity/notificationActions.entity"; -import { SearchFilterDto } from "./dto/searchTemplateType.dto"; -import APIResponse from "src/common/utils/response"; -import { Response } from "express"; -import { CreateEventDto } from "./dto/createTemplate.dto"; -import { NotificationActionTemplates } from "./entity/notificationActionTemplates.entity"; -import { UpdateEventDto } from "./dto/updateEventTemplate.dto"; -import { APIID } from "src/common/utils/api-id.config"; +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NotificationActions } from './entity/notificationActions.entity'; +import { SearchFilterDto } from './dto/searchTemplateType.dto'; +import APIResponse from 'src/common/utils/response'; +import { Response } from 'express'; +import { CreateEventDto } from './dto/createTemplate.dto'; +import { NotificationActionTemplates } from './entity/notificationActionTemplates.entity'; +import { UpdateEventDto } from './dto/updateEventTemplate.dto'; +import { UpdateNotificationActionDto } from './dto/updateNotificationAction.dto'; +import { APIID } from 'src/common/utils/api-id.config'; import { ERROR_MESSAGES, SUCCESS_MESSAGES, -} from "src/common/utils/constant.util"; -import { LoggerUtil } from "src/common/logger/LoggerUtil"; +} from 'src/common/utils/constant.util'; +import { LoggerUtil } from 'src/common/logger/LoggerUtil'; @Injectable() export class NotificationEventsService { constructor( @@ -68,12 +69,12 @@ export class NotificationEventsService { body: configData.body, status: data.status, type: type, - language: "en", + language: 'en', actionId: notificationTemplateResult.actionId, createdBy: userId, updatedBy: userId, - image: type === "push" ? configData?.image || null : null, - link: type === "push" ? configData?.link || null : null, + image: type === 'push' ? configData?.image || null : null, + link: type === 'push' ? configData?.link || null : null, }); return await this.notificationTemplateConfigRepository.save( templateConfig @@ -81,15 +82,15 @@ export class NotificationEventsService { }; if (data.email && Object.keys(data.email).length > 0) { - await createConfig("email", data.email); + await createConfig('email', data.email); } if (data.push && Object.keys(data.push).length > 0) { - await createConfig("push", data.push); + await createConfig('push', data.push); } if (data.sms && Object.keys(data.sms).length > 0) { - await createConfig("sms", data.sms); + await createConfig('sms', data.sms); } LoggerUtil.log( SUCCESS_MESSAGES.TEMPLATE_CREATED_SUCESSFULLY(userId), @@ -98,7 +99,7 @@ export class NotificationEventsService { ); return response .status(HttpStatus.CREATED) - .json(APIResponse.success(apiId, notificationTemplateResult, "Created")); + .json(APIResponse.success(apiId, notificationTemplateResult, 'Created')); } async updateNotificationTemplate( @@ -172,7 +173,7 @@ export class NotificationEventsService { subject: configData.subject, body: configData.body, status: result.status, - language: "en", + language: 'en', updatedBy: userId, createdBy: userId, // getting null constraint error image: configData.image || null, @@ -185,15 +186,15 @@ export class NotificationEventsService { } }; if (updateEventDto.email && Object.keys(updateEventDto.email).length > 0) { - await createConfig("email", updateEventDto.email); + await createConfig('email', updateEventDto.email); } if (updateEventDto.push && Object.keys(updateEventDto.push).length > 0) { - await createConfig("push", updateEventDto.push); + await createConfig('push', updateEventDto.push); } if (updateEventDto.sms && Object.keys(updateEventDto.sms).length > 0) { - await createConfig("sms", updateEventDto.sms); + await createConfig('sms', updateEventDto.sms); } if (updateEventDto.status) { @@ -216,7 +217,7 @@ export class NotificationEventsService { ); return response .status(HttpStatus.OK) - .json(APIResponse.success(apiId, { id: id }, "OK")); + .json(APIResponse.success(apiId, { id: id }, 'OK')); } async getTemplates( @@ -234,7 +235,7 @@ export class NotificationEventsService { } const result = await this.notificationTemplatesRepository.find({ where: whereCondition, - relations: ["templateconfig"], + relations: ['templateconfig'], }); if (result.length === 0) { @@ -253,7 +254,7 @@ export class NotificationEventsService { LoggerUtil.log(SUCCESS_MESSAGES.GET_TEMPLATE(userId), apiId, userId); return response .status(HttpStatus.OK) - .json(APIResponse.success(apiId, finalResult, "OK")); + .json(APIResponse.success(apiId, finalResult, 'OK')); } async deleteTemplate(actionId: number, userId: string, response: Response) { @@ -281,11 +282,197 @@ export class NotificationEventsService { LoggerUtil.log(SUCCESS_MESSAGES.DELETE_TEMPLATE(userId), apiId, userId); return response .status(HttpStatus.OK) - .json(APIResponse.success(apiId, { id: actionId }, "OK")); + .json(APIResponse.success(apiId, { id: actionId }, 'OK')); } + + //update In both action and template table + async updateNotificationAction( + id: number, + updateNotificationActionDto: UpdateNotificationActionDto, + userId: string, + response: Response + ) { + const apiId = APIID.TEMPLATE_UPDATE; + + // Check if NotificationActions record exists + const existingAction = await this.notificationTemplatesRepository.findOne({ + where: { actionId: id }, + }); + + if (!existingAction) { + LoggerUtil.error( + ERROR_MESSAGES.TEMPLATE_NOT_EXIST, + ERROR_MESSAGES.NOT_FOUND, + apiId, + userId + ); + throw new BadRequestException(ERROR_MESSAGES.TEMPLATE_NOT_EXIST); + } + + // Update NotificationActions table fields + const actionUpdates: any = {}; + if (updateNotificationActionDto.title) { + actionUpdates.title = updateNotificationActionDto.title; + } + if (updateNotificationActionDto.status) { + actionUpdates.status = updateNotificationActionDto.status; + } + actionUpdates.updatedBy = userId; + + if (Object.keys(actionUpdates).length > 0) { + await this.notificationTemplatesRepository.update(id, actionUpdates); + } + + // Helper function to update template by type + const updateTemplateByType = async (type: string, configData: any) => { + if (configData && Object.keys(configData).length > 0) { + let existingConfig = + await this.notificationTemplateConfigRepository.findOne({ + where: { actionId: id, type: type }, + }); + + if (existingConfig) { + // Update existing template + Object.assign(existingConfig, configData); + existingConfig.updatedBy = userId; + if (updateNotificationActionDto.templateStatus) { + existingConfig.status = updateNotificationActionDto.templateStatus; + } + return await this.notificationTemplateConfigRepository.save( + existingConfig + ); + } else { + // Create new template if it doesn't exist + if (!configData.subject || !configData.body) { + throw new BadRequestException( + ERROR_MESSAGES.NOT_EMPTY_SUBJECT_OR_BODY + ); + } + const newConfig = this.notificationTemplateConfigRepository.create({ + actionId: id, + type: type, + subject: configData.subject, + body: configData.body, + status: + updateNotificationActionDto.templateStatus || + existingAction.status, + language: 'en', + updatedBy: userId, + createdBy: userId, + image: configData.image || null, + link: configData.link || null, + emailFromName: configData.emailFromName || null, + emailFrom: configData.emailFrom || null, + }); + return await this.notificationTemplateConfigRepository.save( + newConfig + ); + } + } + }; + + // Update templates based on type + if ( + updateNotificationActionDto.email && + Object.keys(updateNotificationActionDto.email).length > 0 + ) { + await updateTemplateByType('email', updateNotificationActionDto.email); + } + + if ( + updateNotificationActionDto.sms && + Object.keys(updateNotificationActionDto.sms).length > 0 + ) { + await updateTemplateByType('sms', updateNotificationActionDto.sms); + } + + if ( + updateNotificationActionDto.push && + Object.keys(updateNotificationActionDto.push).length > 0 + ) { + await updateTemplateByType('push', updateNotificationActionDto.push); + } + + // Update template status for all existing templates if templateStatus is provided + if (updateNotificationActionDto.templateStatus) { + let existingConfigs = + await this.notificationTemplateConfigRepository.find({ + where: { actionId: id }, + }); + existingConfigs.forEach(async (config) => { + config.status = updateNotificationActionDto.templateStatus; + config.updatedBy = userId; + await this.notificationTemplateConfigRepository.save(config); + }); + } + + // Fetch the updated data from both tables + const updatedAction = await this.notificationTemplatesRepository.findOne({ + where: { actionId: id }, + relations: ['templateconfig'], + }); + + if (!updatedAction) { + throw new BadRequestException( + 'Failed to fetch updated notification action' + ); + } + + // Format the response to include both tables data + const { templateconfig, ...actionData } = updatedAction; + const formattedTemplateConfig = templateconfig.reduce((acc, config) => { + const { + type, + language, + subject, + body, + createdOn, + image, + link, + emailFromName, + emailFrom, + status, + } = config; + acc[type] = { + language, + subject, + body, + createdOn, + image, + link, + emailFromName, + emailFrom, + status, + }; + return acc; + }, {}); + + const responseData = { + actionId: id, + ...actionData, + templates: formattedTemplateConfig, + }; + + LoggerUtil.log( + `Notification action updated successfully by userId: ${userId}`, + apiId, + userId + ); + + return response + .status(HttpStatus.OK) + .json( + APIResponse.success( + apiId, + responseData, + 'Notification action updated successfully' + ) + ); + } + getUserIdFromToken(token: string) { - const payloadBase64 = token.split(".")[1]; // Get the payload part - const payloadJson = Buffer.from(payloadBase64, "base64").toString("utf-8"); // Decode Base64 + const payloadBase64 = token.split('.')[1]; // Get the payload part + const payloadJson = Buffer.from(payloadBase64, 'base64').toString('utf-8'); // Decode Base64 const payload = JSON.parse(payloadJson); // Convert to JSON return payload.sub; }