diff --git a/dops/src/helper/auth.helper.ts b/dops/src/helper/auth.helper.ts index ed93e20d3..b288fc2f3 100644 --- a/dops/src/helper/auth.helper.ts +++ b/dops/src/helper/auth.helper.ts @@ -43,21 +43,21 @@ function isRoleArray(obj: Role[] | IRole[]): obj is Role[] { return Array.isArray(obj) && obj.every((item) => typeof item === 'string'); } +/** + * Evaluates if a user has at least one of the specified roles or meets complex role criteria. + * + * This method supports two kinds of inputs for role requirements: + * 1. Simple list of roles (Role[]): In this case, it checks if any of the roles assigned to the user matches at least one of + * the roles specified in the 'roles' parameter. It returns true if there's a match, indicating the user has one of the necessary roles. + * 2. Complex role requirements (IRole[]): When 'roles' is an array of objects implementing the IRole interface (meaning it can specify + * complex role combinations with 'allOf' and 'oneOf' properties), it evaluates these conditions for each role object. It returns true + * if for any role object, either all of the 'allOf' roles or at least one of the 'oneOf' roles are present in the 'userRoles' array. + * + * @param {Role[] | IRole[]} roles - An array of roles or role requirement objects to be matched against the user's roles. + * @param {Role[]} userRoles - An array of roles assigned to the user. + * @returns {boolean} Returns true if the user has at least one of the required roles or meets the complex role requirements, false otherwise. + */ export const matchRoles = (roles: Role[] | IRole[], userRoles: Role[]) => { - /** - * Evaluates if a user has at least one of the specified roles or meets complex role criteria. - * - * This method supports two kinds of inputs for role requirements: - * 1. Simple list of roles (Role[]): In this case, it checks if any of the roles assigned to the user matches at least one of - * the roles specified in the 'roles' parameter. It returns true if there's a match, indicating the user has one of the necessary roles. - * 2. Complex role requirements (IRole[]): When 'roles' is an array of objects implementing the IRole interface (meaning it can specify - * complex role combinations with 'allOf' and 'oneOf' properties), it evaluates these conditions for each role object. It returns true - * if for any role object, either all of the 'allOf' roles or at least one of the 'oneOf' roles are present in the 'userRoles' array. - * - * @param {Role[] | IRole[]} roles - An array of roles or role requirement objects to be matched against the user's roles. - * @param {Role[]} userRoles - An array of roles assigned to the user. - * @returns {boolean} Returns true if the user has at least one of the required roles or meets the complex role requirements, false otherwise. - */ if (isRoleArray(roles)) { // Scenario: roles is a simple list of Role objects. // This block checks if any of the roles assigned to the user (userRoles) diff --git a/dops/src/interface/role.interface.ts b/dops/src/interface/role.interface.ts index 87550deda..a0166cbdb 100644 --- a/dops/src/interface/role.interface.ts +++ b/dops/src/interface/role.interface.ts @@ -1,5 +1,11 @@ import { Role } from '../enum/roles.enum'; +/** + * Defines criteria for role checking. + * @oneOf Optional array of roles where any one role is sufficient + * @allOf Optional array of roles where all roles are required + * Note: Only one of `oneOf` or `allOf` should be specified at any given time. + */ export interface IRole { oneOf?: Role[]; allOf?: Role[]; diff --git a/vehicles/src/common/helper/auth.helper.ts b/vehicles/src/common/helper/auth.helper.ts index 38e902861..5e4cfbeb7 100644 --- a/vehicles/src/common/helper/auth.helper.ts +++ b/vehicles/src/common/helper/auth.helper.ts @@ -43,21 +43,21 @@ function isRoleArray(obj: Role[] | IRole[]): obj is Role[] { return Array.isArray(obj) && obj.every((item) => typeof item === 'string'); } +/** + * Evaluates if a user has at least one of the specified roles or meets complex role criteria. + * + * This method supports two kinds of inputs for role requirements: + * 1. Simple list of roles (Role[]): In this case, it checks if any of the roles assigned to the user matches at least one of + * the roles specified in the 'roles' parameter. It returns true if there's a match, indicating the user has one of the necessary roles. + * 2. Complex role requirements (IRole[]): When 'roles' is an array of objects implementing the IRole interface (meaning it can specify + * complex role combinations with 'allOf' and 'oneOf' properties), it evaluates these conditions for each role object. It returns true + * if for any role object, either all of the 'allOf' roles or at least one of the 'oneOf' roles are present in the 'userRoles' array. + * + * @param {Role[] | IRole[]} roles - An array of roles or role requirement objects to be matched against the user's roles. + * @param {Role[]} userRoles - An array of roles assigned to the user. + * @returns {boolean} Returns true if the user has at least one of the required roles or meets the complex role requirements, false otherwise. + */ export const matchRoles = (roles: Role[] | IRole[], userRoles: Role[]) => { - /** - * Evaluates if a user has at least one of the specified roles or meets complex role criteria. - * - * This method supports two kinds of inputs for role requirements: - * 1. Simple list of roles (Role[]): In this case, it checks if any of the roles assigned to the user matches at least one of - * the roles specified in the 'roles' parameter. It returns true if there's a match, indicating the user has one of the necessary roles. - * 2. Complex role requirements (IRole[]): When 'roles' is an array of objects implementing the IRole interface (meaning it can specify - * complex role combinations with 'allOf' and 'oneOf' properties), it evaluates these conditions for each role object. It returns true - * if for any role object, either all of the 'allOf' roles or at least one of the 'oneOf' roles are present in the 'userRoles' array. - * - * @param {Role[] | IRole[]} roles - An array of roles or role requirement objects to be matched against the user's roles. - * @param {Role[]} userRoles - An array of roles assigned to the user. - * @returns {boolean} Returns true if the user has at least one of the required roles or meets the complex role requirements, false otherwise. - */ if (isRoleArray(roles)) { // Scenario: roles is a simple list of Role objects. // This block checks if any of the roles assigned to the user (userRoles) diff --git a/vehicles/src/common/interface/role.interface.ts b/vehicles/src/common/interface/role.interface.ts index 87550deda..a0166cbdb 100644 --- a/vehicles/src/common/interface/role.interface.ts +++ b/vehicles/src/common/interface/role.interface.ts @@ -1,5 +1,11 @@ import { Role } from '../enum/roles.enum'; +/** + * Defines criteria for role checking. + * @oneOf Optional array of roles where any one role is sufficient + * @allOf Optional array of roles where all roles are required + * Note: Only one of `oneOf` or `allOf` should be specified at any given time. + */ export interface IRole { oneOf?: Role[]; allOf?: Role[]; diff --git a/vehicles/src/modules/common/dops.service.ts b/vehicles/src/modules/common/dops.service.ts index ec4e6d8e0..6e22c79ca 100644 --- a/vehicles/src/modules/common/dops.service.ts +++ b/vehicles/src/modules/common/dops.service.ts @@ -21,6 +21,7 @@ import { ClsService } from 'nestjs-cls'; import { LogAsyncMethodExecution } from '../../common/decorator/log-async-method-execution.decorator'; import { LogMethodExecution } from '../../common/decorator/log-method-execution.decorator'; import { INotificationDocument } from '../../common/interface/notification-document.interface'; +import { ReadNotificationDto } from './dto/response/read-notification.dto'; @Injectable() export class DopsService { @@ -298,6 +299,6 @@ export class DopsService { }); // Return the response data after casting it to the expected type - return dopsResponse.data as { message: string; transactionId: string }; + return dopsResponse.data as ReadNotificationDto; } } diff --git a/vehicles/src/modules/common/dto/request/create-notification.dto.ts b/vehicles/src/modules/common/dto/request/create-notification.dto.ts new file mode 100644 index 000000000..759179296 --- /dev/null +++ b/vehicles/src/modules/common/dto/request/create-notification.dto.ts @@ -0,0 +1,34 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { + ArrayMinSize, + IsEmail, + IsOptional, + IsString, + Length, +} from 'class-validator'; + +export class CreateNotificationDto { + @ApiProperty({ + description: 'Notification email ids.', + example: ['someguy@mycompany.co', 'somegirl@mycompany.co'], + }) + @IsEmail(undefined, { + each: true, + }) + @ArrayMinSize(1) + to: string[]; + + @AutoMap() + @ApiProperty({ + description: 'The fax number to send the notification to.', + required: false, + maxLength: 20, + minLength: 10, + example: '9999999999', + }) + @IsOptional() + @IsString() + @Length(10, 20) + fax?: string; +} diff --git a/vehicles/src/modules/common/dto/response/read-notification.dto.ts b/vehicles/src/modules/common/dto/response/read-notification.dto.ts new file mode 100644 index 000000000..09cfe49cc --- /dev/null +++ b/vehicles/src/modules/common/dto/response/read-notification.dto.ts @@ -0,0 +1,4 @@ +export class ReadNotificationDto { + message: string; + transactionId: string; +} diff --git a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts index d29f100d0..f0ffa727b 100644 --- a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts +++ b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts @@ -42,6 +42,8 @@ export class PaymentService { private dataSource: DataSource, @InjectRepository(Transaction) private transactionRepository: Repository, + @InjectRepository(Receipt) + private receiptRepository: Repository, @InjectRepository(PaymentMethodType) private paymentMethodTypeRepository: Repository, @InjectRepository(PaymentCardType) @@ -352,6 +354,30 @@ export class PaymentService { return readTransactionDto; } + async updateReceiptDocument( + currentUser: IUserJWT, + receiptId: string, + documentId: string, + ) { + const updateResult = await this.receiptRepository + .createQueryBuilder() + .update() + .set({ + receiptDocumentId: documentId, + updatedUser: currentUser.userName, + updatedDateTime: new Date(), + updatedUserDirectory: currentUser.orbcUserDirectory, + updatedUserGuid: currentUser.userGUID, + }) + .where('receiptId = :receiptId', { receiptId: receiptId }) + .execute(); + + if (updateResult.affected === 0) { + throw new InternalServerErrorException('Update failed'); + } + return true; + } + /** * Updates details returned by Payment Gateway in ORBC System. * @param currentUser - The current user object of type {@link IUserJWT} diff --git a/vehicles/src/modules/permit-application-payment/permit/permit.controller.ts b/vehicles/src/modules/permit-application-payment/permit/permit.controller.ts index a531a86a9..a5feeebca 100644 --- a/vehicles/src/modules/permit-application-payment/permit/permit.controller.ts +++ b/vehicles/src/modules/permit-application-payment/permit/permit.controller.ts @@ -8,6 +8,7 @@ import { Query, Res, BadRequestException, + ForbiddenException, } from '@nestjs/common'; import { PermitService } from './permit.service'; import { ExceptionDto } from '../../../common/exception/exception.dto'; @@ -41,6 +42,8 @@ import { } from 'src/common/enum/user-auth-group.enum'; import { ReadPermitMetadataDto } from './dto/response/read-permit-metadata.dto'; import { doesUserHaveAuthGroup } from '../../../common/helper/auth.helper'; +import { CreateNotificationDto } from '../../common/dto/request/create-notification.dto'; +import { ReadNotificationDto } from '../../common/dto/response/read-notification.dto'; @ApiBearerAuth() @ApiTags('Permit') @@ -229,4 +232,51 @@ export class PermitController { ); return permit; } + + /** + * Sends a notification related to a specific permit. + * + * This method checks if the current user belongs to the specified user authentication group before proceeding. + * If the user does not belong to the required auth group, a ForbiddenException is thrown. + * + * @param request The incoming request object containing the current user information. + * @param permitId The ID of the permit to associate the notification with. + * @param createNotificationDto The data transfer object containing the notification details. + * @returns A promise resolved with the details of the sent notification. + */ + @ApiCreatedResponse({ + description: 'The Notification resource with transaction details', + type: ReadNotificationDto, + }) + @ApiOperation({ + summary: 'Send Permit Notification', + description: + 'Sends a notification related to a specific permit after checking user authorization.', + }) + @Roles(Role.SEND_NOTIFICATION) + @Post('/:permitId/notification') + async notification( + @Req() request: Request, + @Param('permitId') permitId: string, + @Body() + createNotificationDto: CreateNotificationDto, + ): Promise { + const currentUser = request.user as IUserJWT; + + // Throws ForbiddenException if user does not belong to the specified user auth group. + if ( + !doesUserHaveAuthGroup( + currentUser.orbcUserAuthGroup, + IDIR_USER_AUTH_GROUP_LIST, + ) + ) { + throw new ForbiddenException(); + } + + return await this.permitService.sendNotification( + currentUser, + permitId, + createNotificationDto, + ); + } } diff --git a/vehicles/src/modules/permit-application-payment/permit/permit.service.ts b/vehicles/src/modules/permit-application-payment/permit/permit.service.ts index 6bf1f2d5b..41bbbf832 100644 --- a/vehicles/src/modules/permit-application-payment/permit/permit.service.ts +++ b/vehicles/src/modules/permit-application-payment/permit/permit.service.ts @@ -9,7 +9,13 @@ import { import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { InjectRepository } from '@nestjs/typeorm'; -import { Brackets, DataSource, Repository, SelectQueryBuilder } from 'typeorm'; +import { + Brackets, + DataSource, + LessThanOrEqual, + Repository, + SelectQueryBuilder, +} from 'typeorm'; import { ReadPermitDto } from './dto/response/read-permit.dto'; import { Permit } from './entities/permit.entity'; import { PermitType } from './entities/permit-type.entity'; @@ -68,6 +74,8 @@ import { IDP } from '../../../common/enum/idp.enum'; import { PermitApplicationOrigin as PermitApplicationOriginEnum } from '../../../common/enum/permit-application-origin.enum'; import { INotificationDocument } from '../../../common/interface/notification-document.interface'; import { ReadFileDto } from '../../common/dto/response/read-file.dto'; +import { CreateNotificationDto } from '../../common/dto/request/create-notification.dto'; +import { ReadNotificationDto } from '../../common/dto/response/read-notification.dto'; @Injectable() export class PermitService { @@ -857,4 +865,202 @@ export class PermitService { companyId, ); } + + /** + * Sends a notification associated with a permit, including generating and sending document(s) based on permit details and transactions. + * It handles fetching the permit details, generating required documents if they don't exist, and constructing a notification request. + * + * @param currentUser The current user's JWT details. + * @param permitId The permit ID for which the notification will be sent. + * @param createNotificationDto DTO containing details such as recipients for the notification. + * @returns The result of the notification sending operation wrapped in a Promise. + */ + @LogAsyncMethodExecution() + public async sendNotification( + currentUser: IUserJWT, + permitId: string, + createNotificationDto: CreateNotificationDto, + ): Promise { + let permitDocumentId: string; + let receiptDocumentId: string; + // Retrieve detailed information about the permit, including company, transactions, and the receipt for notifications + const permit = await this.permitRepository + .createQueryBuilder('permit') + .leftJoinAndSelect('permit.company', 'company') + .innerJoinAndSelect('permit.permitData', 'permitData') + .innerJoinAndSelect('permit.permitTransactions', 'permitTransactions') + .innerJoinAndSelect('permitTransactions.transaction', 'transaction') + .innerJoinAndSelect('transaction.receipt', 'receipt') + .leftJoinAndSelect('permit.applicationOwner', 'applicationOwner') + .leftJoinAndSelect( + 'applicationOwner.userContact', + 'applicationOwnerContact', + ) + .leftJoinAndSelect('permit.issuer', 'issuer') + .leftJoinAndSelect('issuer.userContact', 'issuerOwnerContact') + .where('permit.permitId = :permitId', { + permitId: permitId, + }) + .andWhere('permit.permitNumber IS NOT NULL') + .andWhere('transaction.pgApproved = 1') + .getOne(); + + /** + * If permit not found raise error. + */ + if (!permit) throw new NotFoundException('Valid permit not found.'); + + const companyInfo = permit.company; + + const notificationData: IssuePermitDataNotification = { + companyName: companyInfo.legalName, + }; + + permitDocumentId = permit?.documentId; + receiptDocumentId = + permit?.permitTransactions?.at(0)?.transaction?.receipt + ?.receiptDocumentId; + + //If permit Document or receipt is not attached to the permit + if (!permitDocumentId || !receiptDocumentId) { + const fullNames = await fetchPermitDataDescriptionValuesFromCache( + this.cacheManager, + permit, + ); + + const revisionHistory = await this.permitRepository.find({ + where: [ + { + originalPermitId: permit.originalPermitId, + permitId: LessThanOrEqual(permit.permitId), + }, + ], + order: { permitId: 'DESC' }, + }); + + const permitDataForTemplate = formatTemplateData( + permit, + fullNames, + companyInfo, + revisionHistory, + ); + //Regenerate permit document if not available + if (!permitDocumentId) { + const dopsRequestData: DopsGeneratedDocument = { + templateName: (() => { + switch (permit.permitStatus) { + case ApplicationStatus.ISSUED: + return TemplateName.PERMIT; + case ApplicationStatus.VOIDED: + return TemplateName.PERMIT_VOID; + case ApplicationStatus.REVOKED: + return TemplateName.PERMIT_REVOKED; + } + })(), + generatedDocumentFileName: permitDataForTemplate.permitNumber, + templateData: permitDataForTemplate, + documentsToMerge: permitDataForTemplate.permitData.commodities.map( + (commodity) => { + if (commodity.checked) { + return commodity.condition; + } + }, + ), + }; + const permitDocument = await this.generateDocument( + currentUser, + dopsRequestData, + companyInfo.companyId, + ); + + permitDocumentId = permitDocument.documentId; + + await this.permitRepository + .createQueryBuilder() + .update() + .set({ + documentId: permitDocumentId, + updatedUser: currentUser.userName, + updatedDateTime: new Date(), + updatedUserDirectory: currentUser.orbcUserDirectory, + updatedUserGuid: currentUser.userGUID, + }) + .where('permitId = :permitId', { permitId: permit.permitId }) + .execute(); + } + //Regenerate receipt document if not available + if (!receiptDocumentId) { + const receiptNumber = + permit.permitTransactions?.at(0).transaction.receipt.receiptNumber; + + const dopsRequestData: DopsGeneratedDocument = { + templateName: TemplateName.PAYMENT_RECEIPT, + generatedDocumentFileName: `Receipt_No_${receiptNumber}`, + templateData: { + ...permitDataForTemplate, + // transaction details still needs to be reworked to support multiple permits + pgTransactionId: + permit.permitTransactions[0].transaction.pgTransactionId, + transactionOrderNumber: + permit.permitTransactions[0].transaction.transactionOrderNumber, + transactionAmount: formatAmount( + permit.permitTransactions[0].transaction.transactionTypeId, + permit.permitTransactions[0].transactionAmount, + ), + totalTransactionAmount: formatAmount( + permit.permitTransactions[0].transaction.transactionTypeId, + permit.permitTransactions[0].transaction.totalTransactionAmount, + ), + payerName: + permit.permitIssuedBy === PermitIssuedBy.PPC + ? constants.PPC_FULL_TEXT + : `${permit?.issuer?.userContact?.firstName} ${permit?.issuer?.userContact?.lastName}`, + issuedBy: + permit.permitIssuedBy === PermitIssuedBy.PPC + ? constants.PPC_FULL_TEXT + : constants.SELF_ISSUED, + consolidatedPaymentMethod: ( + await getPaymentCodeFromCache( + this.cacheManager, + permit.permitTransactions[0].transaction.paymentMethodTypeCode, + permit.permitTransactions[0].transaction.paymentCardTypeCode, + ) + ).consolidatedPaymentMethod, + transactionDate: convertUtcToPt( + permit.permitTransactions[0].transaction.transactionSubmitDate, + 'MMM. D, YYYY, hh:mm a Z', + ), + receiptNo: receiptNumber, + }, + }; + const receiptDocument = await this.generateDocument( + currentUser, + dopsRequestData, + companyInfo.companyId, + ); + receiptDocumentId = receiptDocument.documentId; + + await this.paymentService.updateReceiptDocument( + currentUser, + permit?.permitTransactions[0]?.transaction?.receipt?.receiptId, + receiptDocumentId, + ); + } + } + + // Construct the notification document including template name, recipients, subject, data, and related document IDs + const notificationDocument: INotificationDocument = { + templateName: NotificationTemplate.ISSUE_PERMIT, + to: createNotificationDto.to, + subject: 'onRouteBC Permits - ' + companyInfo.legalName, + data: notificationData, + documentIds: [permitDocumentId, receiptDocumentId], + }; + + // Send the constructed notification via the DOPS service and return the result + return await this.dopsService.notificationWithDocumentsFromDops( + currentUser, + notificationDocument, + ); + } }