diff --git a/vehicles/src/app.module.ts b/vehicles/src/app.module.ts index b7fab5bbd..f6a0f4f51 100644 --- a/vehicles/src/app.module.ts +++ b/vehicles/src/app.module.ts @@ -34,6 +34,7 @@ import { CompanySuspendModule } from './modules/company-user-management/company- import { PermitModule } from './modules/permit-application-payment/permit/permit.module'; import { ApplicationModule } from './modules/permit-application-payment/application/application.module'; import { PaymentModule } from './modules/permit-application-payment/payment/payment.module'; +import { PermitReceiptDocumentModule } from './modules/permit-application-payment/permit-receipt-document/permit-receipt-document.module'; import { ShoppingCartModule } from './modules/shopping-cart/shopping-cart.module'; const envPath = path.resolve(process.cwd() + '/../'); @@ -93,6 +94,7 @@ const envPath = path.resolve(process.cwd() + '/../'); AuthModule, PaymentModule, ShoppingCartModule, + PermitReceiptDocumentModule, ApplicationModule, //! Application Module should be imported before PermitModule to avoid URI conflict PermitModule, FeatureFlagsModule, diff --git a/vehicles/src/common/enum/notification-type.enum.ts b/vehicles/src/common/enum/notification-type.enum.ts new file mode 100644 index 000000000..b5c5620cc --- /dev/null +++ b/vehicles/src/common/enum/notification-type.enum.ts @@ -0,0 +1,4 @@ +export enum NotificationType { + EMAIL_PERMIT = 'EMAIL_PERMIT', + EMAIL_RECEIPT = 'EMAIL_RECEIPT', +} diff --git a/vehicles/src/modules/common/dto/request/create-notification.dto.ts b/vehicles/src/modules/common/dto/request/create-notification.dto.ts index 759179296..171d863b8 100644 --- a/vehicles/src/modules/common/dto/request/create-notification.dto.ts +++ b/vehicles/src/modules/common/dto/request/create-notification.dto.ts @@ -1,12 +1,7 @@ import { AutoMap } from '@automapper/classes'; import { ApiProperty } from '@nestjs/swagger'; -import { - ArrayMinSize, - IsEmail, - IsOptional, - IsString, - Length, -} from 'class-validator'; +import { ArrayMinSize, IsEmail, IsEnum } from 'class-validator'; +import { NotificationType } from '../../../../common/enum/notification-type.enum'; export class CreateNotificationDto { @ApiProperty({ @@ -21,14 +16,13 @@ export class CreateNotificationDto { @AutoMap() @ApiProperty({ - description: 'The fax number to send the notification to.', - required: false, - maxLength: 20, - minLength: 10, - example: '9999999999', + enum: NotificationType, + required: true, + description: 'The type of notification.', + isArray: true, + example: [NotificationType.EMAIL_PERMIT, NotificationType.EMAIL_RECEIPT], }) - @IsOptional() - @IsString() - @Length(10, 20) - fax?: string; + @IsEnum(NotificationType, { each: true }) + @ArrayMinSize(1) + notificationType: NotificationType[]; } diff --git a/vehicles/src/modules/common/dto/response/read-notification.dto.ts b/vehicles/src/modules/common/dto/response/read-notification.dto.ts index 09cfe49cc..5d93bc804 100644 --- a/vehicles/src/modules/common/dto/response/read-notification.dto.ts +++ b/vehicles/src/modules/common/dto/response/read-notification.dto.ts @@ -1,4 +1,7 @@ +import { NotificationType } from '../../../../common/enum/notification-type.enum'; + export class ReadNotificationDto { + notificationType?: NotificationType; message: string; transactionId: string; } diff --git a/vehicles/src/modules/permit-application-payment/application/application.controller.ts b/vehicles/src/modules/permit-application-payment/application/application.controller.ts index 8b057fdc6..ff563dda8 100644 --- a/vehicles/src/modules/permit-application-payment/application/application.controller.ts +++ b/vehicles/src/modules/permit-application-payment/application/application.controller.ts @@ -46,6 +46,7 @@ import { DeleteDto } from '../../common/dto/response/delete.dto'; import { PermitApplicationOrigin } from '../../../common/enum/permit-application-origin.enum'; import { ReadApplicationMetadataDto } from './dto/response/read-application-metadata.dto'; import { doesUserHaveAuthGroup } from '../../../common/helper/auth.helper'; +import { PermitReceiptDocumentService } from '../permit-receipt-document/permit-receipt-document.service'; @ApiBearerAuth() @ApiTags('Permit Application') @@ -63,7 +64,10 @@ import { doesUserHaveAuthGroup } from '../../../common/helper/auth.helper'; type: ExceptionDto, }) export class ApplicationController { - constructor(private readonly applicationService: ApplicationService) {} + constructor( + private readonly applicationService: ApplicationService, + private readonly permitReceiptDocumentService: PermitReceiptDocumentService, + ) {} /** * Create Permit application * @param request @@ -268,12 +272,12 @@ export class ApplicationController { if (result?.success?.length) { await Promise.allSettled([ - this.applicationService.generatePermitDocuments( + this.permitReceiptDocumentService.generatePermitDocuments( currentUser, result.success, issuePermitDto.companyId, ), - this.applicationService.generateReceiptDocuments( + this.permitReceiptDocumentService.generateReceiptDocuments( currentUser, result.success, issuePermitDto.companyId, diff --git a/vehicles/src/modules/permit-application-payment/application/application.module.ts b/vehicles/src/modules/permit-application-payment/application/application.module.ts index 6e2c33388..7fe75f9f3 100644 --- a/vehicles/src/modules/permit-application-payment/application/application.module.ts +++ b/vehicles/src/modules/permit-application-payment/application/application.module.ts @@ -9,6 +9,7 @@ import { PaymentModule } from '../payment/payment.module'; import { PermitData } from '../permit/entities/permit-data.entity'; import { PermitType } from '../permit/entities/permit-type.entity'; import { Permit } from '../permit/entities/permit.entity'; +import { PermitReceiptDocumentModule } from '../permit-receipt-document/permit-receipt-document.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { Permit } from '../permit/entities/permit.entity'; PermitApprovalSource, ]), PaymentModule, + PermitReceiptDocumentModule, ], controllers: [ApplicationController], providers: [ApplicationService, ApplicationProfile], diff --git a/vehicles/src/modules/permit-application-payment/application/application.service.ts b/vehicles/src/modules/permit-application-payment/application/application.service.ts index b13f119aa..c47060b80 100644 --- a/vehicles/src/modules/permit-application-payment/application/application.service.ts +++ b/vehicles/src/modules/permit-application-payment/application/application.service.ts @@ -10,13 +10,7 @@ import { NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { - Brackets, - DataSource, - IsNull, - Repository, - SelectQueryBuilder, -} from 'typeorm'; +import { Brackets, DataSource, Repository, SelectQueryBuilder } from 'typeorm'; import { CreateApplicationDto } from './dto/request/create-application.dto'; import { ReadApplicationDto } from './dto/response/read-application.dto'; import { Permit } from '../permit/entities/permit.entity'; @@ -29,17 +23,9 @@ import { PermitApprovalSource as PermitApprovalSourceEnum } from '../../../commo import { paginate, sortQuery } from '../../../common/helper/database.helper'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; -import { NotificationTemplate } from '../../../common/enum/notification-template.enum'; import { DopsService } from '../../common/dops.service'; -import { TemplateName } from '../../../common/enum/template-name.enum'; -import { convertUtcToPt } from '../../../common/helper/date-time.helper'; import { Directory } from '../../../common/enum/directory.enum'; import { PermitIssuedBy } from '../../../common/enum/permit-issued-by.enum'; -import { - formatAmount, - getPaymentCodeFromCache, -} from '../../../common/helper/payment.helper'; -import * as constants from '../../../common/constants/api.constant'; import { LogAsyncMethodExecution } from '../../../common/decorator/log-async-method-execution.decorator'; import { PageMetaDto } from '../../../common/dto/paginate/page-meta'; import { PaginationDto } from '../../../common/dto/paginate/pagination'; @@ -51,7 +37,6 @@ import { import { DeleteDto } from '../../common/dto/response/delete.dto'; import { ReadApplicationMetadataDto } from './dto/response/read-application-metadata.dto'; import { doesUserHaveAuthGroup } from '../../../common/helper/auth.helper'; -import { formatTemplateData } from '../../../common/helper/format-template-data.helper'; import { ACTIVE_APPLICATION_STATUS, ACTIVE_APPLICATION_STATUS_FOR_ISSUANCE, @@ -61,14 +46,10 @@ import { import { IDP } from '../../../common/enum/idp.enum'; import { IUserJWT } from '../../../common/interface/user-jwt.interface'; import { - fetchPermitDataDescriptionValuesFromCache, generateApplicationNumber, generatePermitNumber, } from '../../../common/helper/permit-application.helper'; -import { INotificationDocument } from '../../../common/interface/notification-document.interface'; import { PaymentService } from '../payment/payment.service'; -import { CacheKey } from '../../../common/enum/cache-key.enum'; -import { getFromCache } from '../../../common/helper/cache.helper'; @Injectable() export class ApplicationService { @@ -250,70 +231,6 @@ export class ApplicationService { return await permitQB.getMany(); } - /** - * Finds multiple permits with their transactionId, filtered by application IDs and an optional companyId. Permits are only included if they have a receipt. - * @param applicationIds Array of application IDs to filter permits. - * @param companyId Optional company ID for further filtering. - * @returns Promise resolving to an array of objects, each containing a transactionId and its associated permits. - */ - private async findApplicationsForReceiptGeneration( - applicationIds: string[], - companyId?: number, - ): Promise<{ transactionId: string; permits: Permit[] }[]> { - const permitQB = this.permitRepository.createQueryBuilder('permit'); - permitQB - .select('transaction.transactionId', 'transactionId') - .addSelect( - 'COUNT(permit.permitId) OVER (PARTITION BY transaction.transactionId)', - 'permitCountPerTransactionId', - ) - .distinct(true) - .leftJoin('permit.company', 'company') - .innerJoin('permit.permitTransactions', 'permitTransactions') - .innerJoin('permitTransactions.transaction', 'transaction') - .innerJoin('transaction.receipt', 'receipt') - .where('permit.permitId IN (:...permitIds)', { - permitIds: applicationIds, - }) - .andWhere('receipt.receiptNumber IS NOT NULL') - .andWhere('permit.permitNumber IS NOT NULL'); - - if (companyId) { - permitQB.andWhere('company.companyId = :companyId', { - companyId: companyId, - }); - } - - const transactions = await permitQB.getRawMany<{ - transactionId: string; - permitCountPerTransactionId: number; - }>(); - - const transactionPermitList: { - transactionId: string; - permits: Permit[]; - }[] = []; - - for (const transaction of transactions) { - const fetchedApplications = await this.findManyWithSuccessfulTransaction( - null, - companyId, - transaction.transactionId, - ); - - if ( - fetchedApplications?.length === transaction.permitCountPerTransactionId - ) { - transactionPermitList.push({ - transactionId: transaction.transactionId, - permits: fetchedApplications, - }); - } - } - - return transactionPermitList; - } - /* Get single application By Permit ID*/ @LogAsyncMethodExecution() async findApplication( @@ -668,366 +585,6 @@ export class ApplicationService { } } - /** - * Generates permit documents for a set of application IDs, optionally filtering by company ID. - * The method checks if applications exist and have an ISSUED status, generates document data based - * on the application details, and then updates the permit repository with the document IDs obtained - * from generating documents. It handles and logs errors during document generation and updates. - * It returns a list of application IDs for which document generation succeeded or failed. - * - * @param currentUser - The user who is currently logged in. - * @param applicationIds - Array of application IDs for which to generate documents. - * @param companyId - Optional company ID to filter applications by. - * @returns A Promise resolving to a ResultDto containing lists of application IDs that succeeded - * or failed in document generation. - */ - @LogAsyncMethodExecution() - async generatePermitDocuments( - currentUser: IUserJWT, - applicationIds: string[], - companyId?: number, - ): Promise { - if (!applicationIds?.length) { - throw new InternalServerErrorException( - 'ApplicationId list cannot be empty', - ); - } - - const resultDto: ResultDto = { - success: [], - failure: [], - }; - - const fetchedApplications = await this.findManyWithSuccessfulTransaction( - applicationIds, - companyId, - ); - - if (!fetchedApplications?.length) { - resultDto.failure = applicationIds; - return resultDto; - } - - await Promise.allSettled( - fetchedApplications?.map(async (fetchedApplication) => { - try { - if (fetchedApplication.documentId) { - throw new HttpException('Document already exists', 409); - } - if (fetchedApplication.permitStatus != ApplicationStatus.ISSUED) { - throw new BadRequestException( - 'Application must be in ISSUED status for document Generation!', - ); - } - - const fullNames = await fetchPermitDataDescriptionValuesFromCache( - this.cacheManager, - fetchedApplication, - ); - - const revisionHistory = await this.permitRepository.find({ - where: [{ originalPermitId: fetchedApplication.originalPermitId }], - order: { permitId: 'DESC' }, - }); - - const { company } = fetchedApplication; - - const permitDataForTemplate = formatTemplateData( - fetchedApplication, - fullNames, - company, - revisionHistory, - ); - - const dopsRequestData = { - templateName: TemplateName.PERMIT, - generatedDocumentFileName: permitDataForTemplate.permitNumber, - templateData: permitDataForTemplate, - documentsToMerge: permitDataForTemplate.permitData.commodities.map( - (commodity) => { - if (commodity.checked) { - return commodity.condition; - } - }, - ), - }; - - const generatedDocument = await this.dopsService.generateDocument( - currentUser, - dopsRequestData, - company?.companyId, - ); - - const documentId = generatedDocument?.documentId; - - const updateResult = await this.permitRepository.update( - { permitId: fetchedApplication.permitId, documentId: IsNull() }, - { - documentId: documentId, - updatedDateTime: new Date(), - updatedUser: currentUser.userName, - updatedUserDirectory: currentUser.orbcUserDirectory, - updatedUserGuid: currentUser.userGUID, - }, - ); - if (updateResult.affected === 0) { - throw new InternalServerErrorException( - 'Update permit document failed', - ); - } - - try { - const emailList = [ - permitDataForTemplate.permitData?.contactDetails?.email, - permitDataForTemplate.permitData?.contactDetails?.additionalEmail, - company?.email, - ]; - - const subject = `onRouteBC Permits - ${company?.legalName}`; - this.emailDocument( - NotificationTemplate.ISSUE_PERMIT, - emailList, - subject, - documentId, - currentUser, - ); - } catch (error: unknown) { - /** - * Swallow the error as failure to send notification should not break the flow - */ - this.logger.error(error); - } - - resultDto.success.push(fetchedApplication.permitId); - return Promise.resolve(fetchedApplication); - } catch (error: unknown) { - this.logger.error(error); - resultDto.failure.push(fetchedApplication.permitId); - // Return the error for failed operations - return Promise.reject(error as Error); - } - }), - ); - - if (resultDto?.failure?.length) { - this.logger.error( - `Failed Permit Document Generation: ${resultDto?.failure?.toString()}`, - ); - } - - return resultDto; - } - - /** - * Generates receipt documents for the provided application IDs and optionally filters by company ID. - * Each receipt document corresponds to a transaction within an application permit. - * The method attempts to generate a receipt document for each permit associated with the provided application - * IDs, handling document existence checks, data formatting for the document template, and updating receipt IDs with - * the generated document IDs. It also attempts to send out emails with the generated document. Successes and failures - * are tracked and returned in the result. - * - * @param currentUser - The user currently logged in. - * @param applicationIds - Array of application IDs to generate receipt documents for. - * @param companyId - Optional company ID to filter applications by. - * @returns A Promise of a ResultDto indicating which operations succeeded or failed. - */ - @LogAsyncMethodExecution() - async generateReceiptDocuments( - currentUser: IUserJWT, - applicationIds: string[], - companyId?: number, - ): Promise { - if (!applicationIds?.length) { - throw new InternalServerErrorException( - 'ApplicationId list cannot be empty', - ); - } - - const resultDto: ResultDto = { - success: [], - failure: [], - }; - - const fetchedApplications = await this.findApplicationsForReceiptGeneration( - applicationIds, - companyId, - ); - - if (!fetchedApplications?.length) { - resultDto.failure = applicationIds; - return resultDto; - } - - await Promise.allSettled( - fetchedApplications?.map(async (fetchedApplication) => { - const permits = fetchedApplication.permits; - const permitIds = permits?.map((permit) => permit.permitId); - if (permits?.length) { - try { - const permit = permits?.at(0); - const company = permit?.company; - const permitTransactions = permit?.permitTransactions; - const transaction = permitTransactions?.at(0)?.transaction; - const receipt = transaction?.receipt; - if (receipt.receiptDocumentId) { - throw new HttpException('Document already exists', 409); - } - - const receiptNumber = receipt.receiptNumber; - - const fullNames = await fetchPermitDataDescriptionValuesFromCache( - this.cacheManager, - permit, - ); - - const { companyName, companyAlternateName, permitData } = - formatTemplateData(permit, fullNames, company); - const permitDetails = await Promise.all( - permits?.map(async (permit) => { - return { - permitName: await getFromCache( - this.cacheManager, - CacheKey.PERMIT_TYPE, - permit?.permitType, - ), - permitNumber: permit?.permitNumber, - transactionAmount: formatAmount( - transaction?.transactionTypeId, - permit?.permitTransactions?.at(0)?.transactionAmount, - ), - }; - }), - ); - - const dopsRequestData = { - templateName: TemplateName.PAYMENT_RECEIPT, - generatedDocumentFileName: `Receipt_No_${receiptNumber}`, - templateData: { - receiptNo: receiptNumber, - companyName: companyName, - companyAlternateName: companyAlternateName, - permitData: permitData, - //Payer Name should be persisted in transacation Table so that it can be used for DocRegen - payerName: - currentUser.orbcUserDirectory === Directory.IDIR - ? constants.PPC_FULL_TEXT - : currentUser.orbcUserFirstName + - ' ' + - currentUser.orbcUserLastName, - issuedBy: - currentUser.orbcUserDirectory === Directory.IDIR - ? constants.PPC_FULL_TEXT - : constants.SELF_ISSUED, - totalTransactionAmount: formatAmount( - transaction?.transactionTypeId, - transaction?.totalTransactionAmount, - ), - permitDetails: permitDetails, - //Transaction Details - pgTransactionId: transaction?.pgTransactionId, - transactionOrderNumber: transaction?.transactionOrderNumber, - consolidatedPaymentMethod: ( - await getPaymentCodeFromCache( - this.cacheManager, - transaction?.paymentMethodTypeCode, - transaction?.paymentCardTypeCode, - ) - ).consolidatedPaymentMethod, - transactionDate: convertUtcToPt( - permit?.permitTransactions?.at(0)?.transaction - ?.transactionSubmitDate, - 'MMM. D, YYYY, hh:mm a Z', - ), - }, - }; - - const { documentId } = await this.dopsService.generateDocument( - currentUser, - dopsRequestData, - company?.companyId, - ); - - await this.paymentService.updateReceiptDocument( - currentUser, - receipt?.receiptId, - documentId, - ); - - try { - const emailList = [ - permitData?.contactDetails?.email, - permitData?.contactDetails?.additionalEmail, - company?.email, - ]; - const subject = `onRouteBC Permit Receipt - ${receiptNumber}`; - this.emailDocument( - NotificationTemplate.PAYMENT_RECEIPT, - emailList, - subject, - documentId, - currentUser, - ); - } catch (error: unknown) { - /** - * Swallow the error as failure to send notification should not break the flow - */ - this.logger.error(error); - } - resultDto.success.push(...permitIds); - - return Promise.resolve(fetchedApplication); - } catch (error: unknown) { - this.logger.error(error); - resultDto.failure.push(...permitIds); - // Return the error for failed operations - return Promise.reject(error as Error); - } - } - }), - ); - applicationIds?.forEach((id) => { - if ( - !resultDto?.success?.includes(id) && - !resultDto.failure?.includes(id) - ) { - resultDto?.failure?.push(id); - } - }); - - if (resultDto?.failure?.length) { - this.logger.error( - `Failed Permit Receipt Document Generation: ${resultDto?.failure?.toString()}`, - ); - } - - return resultDto; - } - - private emailDocument( - notificationTemplate: - | NotificationTemplate.ISSUE_PERMIT - | NotificationTemplate.PAYMENT_RECEIPT, - to: string[], - subject: string, - documentId: string, - currentUser: IUserJWT, - ) { - const distinctEmailList = Array.from(new Set(to?.filter(Boolean))); - - const notificationDocument: INotificationDocument = { - templateName: notificationTemplate, - to: distinctEmailList, - subject: subject, - documentIds: [documentId], - }; - - void this.dopsService.notificationWithDocumentsFromDops( - currentUser, - notificationDocument, - true, - ); - } - /** * Get Application Origin Code from database lookup table ORBC_VT_PERMIT_APPLICATION_ORIGIN * Retrieves all application origin records from the database. diff --git a/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.module.ts b/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.module.ts new file mode 100644 index 000000000..2a511ebe4 --- /dev/null +++ b/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Permit } from '../permit/entities/permit.entity'; +import { PermitReceiptDocumentService } from './permit-receipt-document.service'; +import { PaymentModule } from '../payment/payment.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([Permit]), PaymentModule], + providers: [PermitReceiptDocumentService], + exports: [PermitReceiptDocumentService], +}) +export class PermitReceiptDocumentModule {} diff --git a/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.service.ts b/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.service.ts new file mode 100644 index 000000000..e4fa33bab --- /dev/null +++ b/vehicles/src/modules/permit-application-payment/permit-receipt-document/permit-receipt-document.service.ts @@ -0,0 +1,550 @@ +import { + BadRequestException, + HttpException, + Inject, + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { InjectRepository } from '@nestjs/typeorm'; +import { IsNull, Repository } from 'typeorm'; +import { TemplateName } from 'src/common/enum/template-name.enum'; +import { convertUtcToPt } from 'src/common/helper/date-time.helper'; +import { NotificationTemplate } from 'src/common/enum/notification-template.enum'; +import { Directory } from 'src/common/enum/directory.enum'; + +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { CacheKey } from 'src/common/enum/cache-key.enum'; +import { getFromCache } from 'src/common/helper/cache.helper'; +import { Cache } from 'cache-manager'; + +import { + formatAmount, + getPaymentCodeFromCache, +} from '../../../common/helper/payment.helper'; +import { LogAsyncMethodExecution } from '../../../common/decorator/log-async-method-execution.decorator'; +import * as constants from '../../../common/constants/api.constant'; +import { formatTemplateData } from '../../../common/helper/format-template-data.helper'; +import { fetchPermitDataDescriptionValuesFromCache } from '../../../common/helper/permit-application.helper'; +import { INotificationDocument } from '../../../common/interface/notification-document.interface'; +import { Permit } from '../permit/entities/permit.entity'; +import { IUserJWT } from '../../../common/interface/user-jwt.interface'; +import { DopsService } from '../../common/dops.service'; +import { ResultDto } from '../permit/dto/response/result.dto'; +import { ApplicationStatus } from '../../../common/enum/application-status.enum'; +import { PaymentService } from '../payment/payment.service'; + +@Injectable() +export class PermitReceiptDocumentService { + private readonly logger = new Logger(PermitReceiptDocumentService.name); + constructor( + @InjectMapper() private readonly classMapper: Mapper, + @InjectRepository(Permit) + private permitRepository: Repository, + private readonly dopsService: DopsService, + private readonly paymentService: PaymentService, + @Inject(CACHE_MANAGER) + private readonly cacheManager: Cache, + ) {} + + /** + * Finds multiple permits with their transactionId, filtered by application IDs and an optional companyId. Permits are only included if they have a receipt. + * @param applicationIds Array of application IDs to filter permits. + * @param companyId Optional company ID for further filtering. + * @returns Promise resolving to an array of objects, each containing a transactionId and its associated permits. + */ + private async findApplicationsForReceiptGeneration( + applicationIds: string[], + companyId?: number, + ): Promise<{ transactionId: string; permits: Permit[] }[]> { + const permitQB = this.permitRepository.createQueryBuilder('permit'); + permitQB + .select('transaction.transactionId', 'transactionId') + .addSelect( + 'COUNT(permit.permitId) OVER (PARTITION BY transaction.transactionId)', + 'permitCountPerTransactionId', + ) + .distinct(true) + .leftJoin('permit.company', 'company') + .innerJoin('permit.permitTransactions', 'permitTransactions') + .innerJoin('permitTransactions.transaction', 'transaction') + .innerJoin('transaction.receipt', 'receipt') + .where('permit.permitId IN (:...permitIds)', { + permitIds: applicationIds, + }) + .andWhere('receipt.receiptNumber IS NOT NULL') + .andWhere('permit.permitNumber IS NOT NULL'); + + if (companyId) { + permitQB.andWhere('company.companyId = :companyId', { + companyId: companyId, + }); + } + + const transactions = await permitQB.getRawMany<{ + transactionId: string; + permitCountPerTransactionId: number; + }>(); + + const transactionPermitList: { + transactionId: string; + permits: Permit[]; + }[] = []; + + for (const transaction of transactions) { + const fetchedApplications = await this.findManyWithSuccessfulTransaction( + null, + companyId, + transaction.transactionId, + ); + + if ( + fetchedApplications?.length === transaction.permitCountPerTransactionId + ) { + transactionPermitList.push({ + transactionId: transaction.transactionId, + permits: fetchedApplications, + }); + } + } + + return transactionPermitList; + } + + /** + * Finds multiple permits by application IDs or a single transaction ID with successful transactions, + * optionally filtering by companyId. + * + * @param applicationIds Array of application IDs to filter the permits. If empty, will search by transactionId. + * @param companyId The ID of the company to which the permits may belong, optional. + * @param transactionId A specific transaction ID to find the related permit, optional. If provided, applicationIds should be empty. + * @returns A promise that resolves with an array of permits matching the criteria. + */ + private async findManyWithSuccessfulTransaction( + permitIds: string[], + companyId?: number, + transactionId?: string, + ): Promise { + if ( + (!permitIds?.length && !transactionId) || + (permitIds?.length && transactionId) + ) { + throw new InternalServerErrorException( + 'Either permitIds or transactionId must be exclusively present!', + ); + } + const permitQB = this.permitRepository.createQueryBuilder('permit'); + permitQB + .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', + ) + .where('receipt.receiptNumber IS NOT NULL') + .where('permit.permitNumber IS NOT NULL'); + + if (permitIds?.length) { + permitQB.andWhere('permit.permitId IN (:...permitIds)', { + permitIds: permitIds, + }); + } else if (transactionId) { + permitQB.andWhere('transaction.transactionId =:transactionId', { + transactionId: transactionId, + }); + } + if (companyId) { + permitQB.andWhere('company.companyId = :companyId', { + companyId: companyId, + }); + } + + return await permitQB.getMany(); + } + + private emailDocument( + notificationTemplate: + | NotificationTemplate.ISSUE_PERMIT + | NotificationTemplate.PAYMENT_RECEIPT, + to: string[], + subject: string, + documentId: string, + currentUser: IUserJWT, + ) { + const distinctEmailList = Array.from(new Set(to?.filter(Boolean))); + + const notificationDocument: INotificationDocument = { + templateName: notificationTemplate, + to: distinctEmailList, + subject: subject, + documentIds: [documentId], + }; + + void this.dopsService.notificationWithDocumentsFromDops( + currentUser, + notificationDocument, + true, + ); + } + + /** + * Generates permit documents for a set of application IDs, optionally filtering by company ID. + * The method checks if applications exist and have an ISSUED status, generates document data based + * on the application details, and then updates the permit repository with the document IDs obtained + * from generating documents. It handles and logs errors during document generation and updates. + * It returns a list of application IDs for which document generation succeeded or failed. + * + * @param currentUser - The user who is currently logged in. + * @param applicationIds - Array of application IDs for which to generate documents. + * @param companyId - Optional company ID to filter applications by. + * @returns A Promise resolving to a ResultDto containing lists of application IDs that succeeded + * or failed in document generation. + */ + @LogAsyncMethodExecution() + async generatePermitDocuments( + currentUser: IUserJWT, + permitIds: string[], + companyId?: number, + ): Promise { + if (!permitIds?.length) { + throw new InternalServerErrorException('permitIds list cannot be empty'); + } + + const resultDto: ResultDto = { + success: [], + failure: [], + }; + + const fetchedPermits = await this.findManyWithSuccessfulTransaction( + permitIds, + companyId, + ); + + if (!fetchedPermits?.length) { + resultDto.failure = permitIds; + return resultDto; + } + + await Promise.allSettled( + fetchedPermits?.map(async (fetchedPermit) => { + try { + if (fetchedPermit.documentId) { + throw new HttpException('Document already exists', 409); + } + if ( + fetchedPermit.permitStatus !== ApplicationStatus.ISSUED && + fetchedPermit.permitStatus !== ApplicationStatus.VOIDED && + fetchedPermit.permitStatus !== ApplicationStatus.REVOKED + ) { + throw new BadRequestException( + 'Application must be in ISSUED/VOIDED/REVOKED status for document Generation!', + ); + } + + const fullNames = await fetchPermitDataDescriptionValuesFromCache( + this.cacheManager, + fetchedPermit, + ); + + const revisionHistory = await this.permitRepository.find({ + where: [{ originalPermitId: fetchedPermit.originalPermitId }], + order: { permitId: 'DESC' }, + }); + + const { company } = fetchedPermit; + + const permitDataForTemplate = formatTemplateData( + fetchedPermit, + fullNames, + company, + revisionHistory, + ); + + const dopsRequestData = { + templateName: (() => { + switch (fetchedPermit.permitStatus) { + case ApplicationStatus.ISSUED: + return TemplateName.PERMIT; + case ApplicationStatus.VOIDED: + return TemplateName.PERMIT_VOID; + case ApplicationStatus.REVOKED: + return TemplateName.PERMIT_REVOKED; + default: + // Handle the default case here, for example: + throw new InternalServerErrorException( + 'Invalid status for document generation', + ); + } + })(), + generatedDocumentFileName: permitDataForTemplate.permitNumber, + templateData: permitDataForTemplate, + documentsToMerge: permitDataForTemplate.permitData.commodities.map( + (commodity) => { + if (commodity.checked) { + return commodity.condition; + } + }, + ), + }; + + const generatedDocument = await this.dopsService.generateDocument( + currentUser, + dopsRequestData, + company?.companyId, + ); + + const documentId = generatedDocument?.documentId; + + const updateResult = await this.permitRepository.update( + { permitId: fetchedPermit.permitId, documentId: IsNull() }, + { + documentId: documentId, + updatedDateTime: new Date(), + updatedUser: currentUser.userName, + updatedUserDirectory: currentUser.orbcUserDirectory, + updatedUserGuid: currentUser.userGUID, + }, + ); + if (updateResult.affected === 0) { + throw new InternalServerErrorException( + 'Update permit document failed', + ); + } + + try { + const emailList = [ + permitDataForTemplate.permitData?.contactDetails?.email, + permitDataForTemplate.permitData?.contactDetails?.additionalEmail, + company?.email, + ]; + + const subject = `onRouteBC Permits - ${company?.legalName}`; + this.emailDocument( + NotificationTemplate.ISSUE_PERMIT, + emailList, + subject, + documentId, + currentUser, + ); + } catch (error: unknown) { + /** + * Swallow the error as failure to send notification should not break the flow + */ + this.logger.error(error); + } + + resultDto.success.push(fetchedPermit.permitId); + return Promise.resolve(fetchedPermit); + } catch (error: unknown) { + this.logger.error(error); + resultDto.failure.push(fetchedPermit.permitId); + // Return the error for failed operations + const rejectionReason = + error instanceof Error ? error : new Error(String(error)); + return Promise.reject(rejectionReason); + } + }), + ); + + if (resultDto?.failure?.length) { + this.logger.error( + `Failed Permit Document Generation: ${resultDto?.failure?.toString()}`, + ); + } + + return resultDto; + } + + /** + * Generates receipt documents for the provided application IDs and optionally filters by company ID. + * Each receipt document corresponds to a transaction within an application permit. + * The method attempts to generate a receipt document for each permit associated with the provided application + * IDs, handling document existence checks, data formatting for the document template, and updating receipt IDs with + * the generated document IDs. It also attempts to send out emails with the generated document. Successes and failures + * are tracked and returned in the result. + * + * @param currentUser - The user currently logged in. + * @param applicationIds - Array of application IDs to generate receipt documents for. + * @param companyId - Optional company ID to filter applications by. + * @returns A Promise of a ResultDto indicating which operations succeeded or failed. + */ + @LogAsyncMethodExecution() + async generateReceiptDocuments( + currentUser: IUserJWT, + permitIds: string[], + companyId?: number, + ): Promise { + if (!permitIds?.length) { + throw new InternalServerErrorException( + 'ApplicationId list cannot be empty', + ); + } + + const resultDto: ResultDto = { + success: [], + failure: [], + }; + + const fetchedPermits = await this.findApplicationsForReceiptGeneration( + permitIds, + companyId, + ); + + if (!fetchedPermits?.length) { + resultDto.failure = permitIds; + return resultDto; + } + + await Promise.allSettled( + fetchedPermits?.map(async (fetchedPermit) => { + const permits = fetchedPermit.permits; + const permitIds = permits?.map((permit) => permit.permitId); + if (permits?.length) { + try { + const permit = permits?.at(0); + const company = permit?.company; + const permitTransactions = permit?.permitTransactions; + const transaction = permitTransactions?.at(0)?.transaction; + const receipt = transaction?.receipt; + if (receipt.receiptDocumentId) { + throw new HttpException('Document already exists', 409); + } + + const receiptNumber = receipt.receiptNumber; + + const fullNames = await fetchPermitDataDescriptionValuesFromCache( + this.cacheManager, + permit, + ); + + const { companyName, companyAlternateName, permitData } = + formatTemplateData(permit, fullNames, company); + const permitDetails = await Promise.all( + permits?.map(async (permit) => { + return { + permitName: await getFromCache( + this.cacheManager, + CacheKey.PERMIT_TYPE, + permit?.permitType, + ), + permitNumber: permit?.permitNumber, + transactionAmount: formatAmount( + transaction?.transactionTypeId, + permit?.permitTransactions?.at(0)?.transactionAmount, + ), + }; + }), + ); + + const dopsRequestData = { + templateName: TemplateName.PAYMENT_RECEIPT, + generatedDocumentFileName: `Receipt_No_${receiptNumber}`, + templateData: { + receiptNo: receiptNumber, + companyName: companyName, + companyAlternateName: companyAlternateName, + permitData: permitData, + //Payer Name should be persisted in transacation Table so that it can be used for DocRegen + payerName: + currentUser.orbcUserDirectory === Directory.IDIR + ? constants.PPC_FULL_TEXT + : currentUser.orbcUserFirstName + + ' ' + + currentUser.orbcUserLastName, + issuedBy: + currentUser.orbcUserDirectory === Directory.IDIR + ? constants.PPC_FULL_TEXT + : constants.SELF_ISSUED, + totalTransactionAmount: formatAmount( + transaction?.transactionTypeId, + transaction?.totalTransactionAmount, + ), + permitDetails: permitDetails, + //Transaction Details + pgTransactionId: transaction?.pgTransactionId, + transactionOrderNumber: transaction?.transactionOrderNumber, + consolidatedPaymentMethod: ( + await getPaymentCodeFromCache( + this.cacheManager, + transaction?.paymentMethodTypeCode, + transaction?.paymentCardTypeCode, + ) + ).consolidatedPaymentMethod, + transactionDate: convertUtcToPt( + permit?.permitTransactions?.at(0)?.transaction + ?.transactionSubmitDate, + 'MMM. D, YYYY, hh:mm a Z', + ), + }, + }; + + const { documentId } = await this.dopsService.generateDocument( + currentUser, + dopsRequestData, + company?.companyId, + ); + + await this.paymentService.updateReceiptDocument( + currentUser, + receipt?.receiptId, + documentId, + ); + + try { + const emailList = [ + permitData?.contactDetails?.email, + permitData?.contactDetails?.additionalEmail, + company?.email, + ]; + const subject = `onRouteBC Permit Receipt - ${receiptNumber}`; + this.emailDocument( + NotificationTemplate.PAYMENT_RECEIPT, + emailList, + subject, + documentId, + currentUser, + ); + } catch (error: unknown) { + /** + * Swallow the error as failure to send notification should not break the flow + */ + this.logger.error(error); + } + resultDto.success.push(...permitIds); + + return Promise.resolve(fetchedPermit); + } catch (error: unknown) { + this.logger.error(error); + resultDto.failure.push(...permitIds); + // Return the error for failed operations + const rejectionReason = + error instanceof Error ? error : new Error(String(error)); + return Promise.reject(rejectionReason); + } + } + }), + ); + permitIds?.forEach((id) => { + if ( + !resultDto?.success?.includes(id) && + !resultDto.failure?.includes(id) + ) { + resultDto?.failure?.push(id); + } + }); + + if (resultDto?.failure?.length) { + this.logger.error( + `Failed Permit Receipt Document Generation: ${resultDto?.failure?.toString()}`, + ); + } + + return resultDto; + } +} 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 a5feeebca..bad369a43 100644 --- a/vehicles/src/modules/permit-application-payment/permit/permit.controller.ts +++ b/vehicles/src/modules/permit-application-payment/permit/permit.controller.ts @@ -44,6 +44,7 @@ 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'; +import { PermitReceiptDocumentService } from '../permit-receipt-document/permit-receipt-document.service'; @ApiBearerAuth() @ApiTags('Permit') @@ -61,7 +62,10 @@ import { ReadNotificationDto } from '../../common/dto/response/read-notification }) @Controller('permits') export class PermitController { - constructor(private readonly permitService: PermitService) {} + constructor( + private readonly permitService: PermitService, + private readonly permitReceiptDocumentService: PermitReceiptDocumentService, + ) {} @ApiOkResponse({ description: 'The Permit Resource to get revision and payment history.', @@ -225,12 +229,24 @@ export class PermitController { voidPermitDto: VoidPermitDto, ): Promise { const currentUser = request.user as IUserJWT; - const permit = await this.permitService.voidPermit( + const { result, voidRevokedPermitId } = await this.permitService.voidPermit( permitId, voidPermitDto, currentUser, ); - return permit; + + if (voidRevokedPermitId) { + await Promise.allSettled([ + this.permitReceiptDocumentService.generatePermitDocuments(currentUser, [ + voidRevokedPermitId, + ]), + this.permitReceiptDocumentService.generateReceiptDocuments( + currentUser, + [voidRevokedPermitId], + ), + ]); + } + return result; } /** @@ -260,9 +276,8 @@ export class PermitController { @Param('permitId') permitId: string, @Body() createNotificationDto: CreateNotificationDto, - ): Promise { + ): Promise { const currentUser = request.user as IUserJWT; - // Throws ForbiddenException if user does not belong to the specified user auth group. if ( !doesUserHaveAuthGroup( diff --git a/vehicles/src/modules/permit-application-payment/permit/permit.module.ts b/vehicles/src/modules/permit-application-payment/permit/permit.module.ts index a101c8c3e..33caeaee9 100644 --- a/vehicles/src/modules/permit-application-payment/permit/permit.module.ts +++ b/vehicles/src/modules/permit-application-payment/permit/permit.module.ts @@ -8,11 +8,13 @@ import { PermitData } from './entities/permit-data.entity'; import { Permit } from './entities/permit.entity'; import { PermitType } from './entities/permit-type.entity'; import { PaymentModule } from '../payment/payment.module'; +import { PermitReceiptDocumentModule } from '../permit-receipt-document/permit-receipt-document.module'; @Module({ imports: [ TypeOrmModule.forFeature([Permit, PermitData, PermitType]), PaymentModule, + PermitReceiptDocumentModule, ], controllers: [PermitController], providers: [PermitService, PermitProfile], 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 820d70225..4bd54e45f 100644 --- a/vehicles/src/modules/permit-application-payment/permit/permit.service.ts +++ b/vehicles/src/modules/permit-application-payment/permit/permit.service.ts @@ -9,13 +9,7 @@ import { import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { InjectRepository } from '@nestjs/typeorm'; -import { - Brackets, - DataSource, - LessThanOrEqual, - Repository, - SelectQueryBuilder, -} from 'typeorm'; +import { Brackets, DataSource, 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'; @@ -24,7 +18,6 @@ import { FileDownloadModes } from '../../../common/enum/file-download-modes.enum import { IUserJWT } from '../../../common/interface/user-jwt.interface'; import { Response } from 'express'; import { PermitStatus } from 'src/common/enum/permit-status.enum'; -import { Receipt } from '../payment/entities/receipt.entity'; import { PaginationDto } from 'src/common/dto/paginate/pagination'; import { PermitHistoryDto } from './dto/response/permit-history.dto'; import { @@ -32,14 +25,11 @@ import { ApplicationStatus, } from 'src/common/enum/application-status.enum'; import { DopsGeneratedDocument } from 'src/common/interface/dops-generated-document.interface'; -import { TemplateName } from 'src/common/enum/template-name.enum'; -import { convertUtcToPt } from 'src/common/helper/date-time.helper'; import { NotificationTemplate } from 'src/common/enum/notification-template.enum'; import { ResultDto } from './dto/response/result.dto'; import { VoidPermitDto } from './dto/request/void-permit.dto'; import { PaymentService } from '../payment/payment.service'; import { CreateTransactionDto } from '../payment/dto/request/create-transaction.dto'; -import { Transaction } from '../payment/entities/transaction.entity'; import { Directory } from 'src/common/enum/directory.enum'; import { PermitData } from './entities/permit-data.entity'; import { Base } from '../../common/entities/base.entity'; @@ -48,14 +38,9 @@ import { CacheKey } from 'src/common/enum/cache-key.enum'; import { getMapFromCache } from 'src/common/helper/cache.helper'; import { Cache } from 'cache-manager'; import { PermitIssuedBy } from '../../../common/enum/permit-issued-by.enum'; -import { - formatAmount, - getPaymentCodeFromCache, -} from '../../../common/helper/payment.helper'; import { PaymentMethodType } from 'src/common/enum/payment-method-type.enum'; import { PageMetaDto } from 'src/common/dto/paginate/page-meta'; import { LogAsyncMethodExecution } from '../../../common/decorator/log-async-method-execution.decorator'; -import * as constants from '../../../common/constants/api.constant'; import { PermitApprovalSource } from '../../../common/enum/permit-approval-source.enum'; import { PermitSearch } from '../../../common/enum/permit-search.enum'; import { paginate, sortQuery } from '../../../common/helper/database.helper'; @@ -63,18 +48,16 @@ import { IDIR_USER_AUTH_GROUP_LIST } from '../../../common/enum/user-auth-group. import { User } from '../../company-user-management/users/entities/user.entity'; import { ReadPermitMetadataDto } from './dto/response/read-permit-metadata.dto'; import { doesUserHaveAuthGroup } from '../../../common/helper/auth.helper'; -import { formatTemplateData } from '../../../common/helper/format-template-data.helper'; import { - fetchPermitDataDescriptionValuesFromCache, generateApplicationNumber, generatePermitNumber, } from '../../../common/helper/permit-application.helper'; 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'; +import { NotificationType } from '../../../common/enum/notification-type.enum'; @Injectable() export class PermitService { @@ -507,7 +490,7 @@ export class PermitService { permitId: string, voidPermitDto: VoidPermitDto, currentUser: IUserJWT, - ): Promise { + ): Promise<{ result: ResultDto; voidRevokedPermitId: string }> { const permit = await this.findOne(permitId); /** * If permit not found raise error. @@ -547,6 +530,7 @@ export class PermitService { let success = ''; let failure = ''; + let voidRevokedPermitId: string = null; const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); @@ -567,6 +551,7 @@ export class PermitService { let newPermit = new Permit(); newPermit = Object.assign(newPermit, permit); newPermit.permitId = null; + newPermit.documentId = null; newPermit.permitNumber = permitNumber; newPermit.applicationNumber = applicationNumber; newPermit.permitStatus = voidPermitDto.status; @@ -630,195 +615,15 @@ export class PermitService { transactionAmount: voidPermitDto.transactionAmount, }, ]; - const transactionDto = await this.paymentService.createTransactions( + await this.paymentService.createTransactions( currentUser, createTransactionDto, queryRunner, ); - const fetchedTransaction = await queryRunner.manager.findOne( - Transaction, - { - where: { transactionId: transactionDto.transactionId }, - relations: ['receipt'], - }, - ); - - const companyInfo = newPermit.company; - - const fullNames = await fetchPermitDataDescriptionValuesFromCache( - this.cacheManager, - newPermit, - ); - - const revisionHistory = await queryRunner.manager.find(Permit, { - where: { originalPermitId: permit.originalPermitId }, - order: { permitId: 'DESC' }, - }); - - const permitDataForTemplate = formatTemplateData( - newPermit, - fullNames, - companyInfo, - revisionHistory, - ); - - let dopsRequestData: DopsGeneratedDocument = { - templateName: - voidPermitDto.status == ApplicationStatus.VOIDED - ? TemplateName.PERMIT_VOID - : TemplateName.PERMIT_REVOKED, - generatedDocumentFileName: permitDataForTemplate.permitNumber, - templateData: permitDataForTemplate, - documentsToMerge: permitDataForTemplate.permitData.commodities.map( - (commodity) => { - if (commodity.checked) { - return commodity.condition; - } - }, - ), - }; - - const generatedPermitDocumentPromise = this.dopsService.generateDocument( - currentUser, - dopsRequestData, - companyInfo?.companyId, - ); - - dopsRequestData = { - templateName: TemplateName.PAYMENT_RECEIPT, - generatedDocumentFileName: `Receipt_No_${fetchedTransaction.receipt.receiptNumber}`, - templateData: { - receiptNo: fetchedTransaction.receipt.receiptNumber, - companyName: permitDataForTemplate.companyName, - companyAlternateName: permitDataForTemplate.companyAlternateName, - permitData: permitDataForTemplate.permitData, - payerName: - currentUser.orbcUserDirectory === Directory.IDIR - ? 'Provincial Permit Centre' - : currentUser.orbcUserFirstName + - ' ' + - currentUser.orbcUserLastName, - issuedBy: - currentUser.orbcUserDirectory === Directory.IDIR - ? constants.PPC_FULL_TEXT - : constants.SELF_ISSUED, - totalTransactionAmount: formatAmount( - fetchedTransaction.transactionTypeId, - fetchedTransaction.totalTransactionAmount, - ), - transactionAmount: formatAmount( - fetchedTransaction.transactionTypeId, - fetchedTransaction.totalTransactionAmount, - ), - permitDetails: [ - { - permitName: permitDataForTemplate.permitName, - permitNumber: permitDataForTemplate.permitNumber, - transactionAmount: formatAmount( - fetchedTransaction.transactionTypeId, - fetchedTransaction.totalTransactionAmount, - ), - }, - ], - //Transaction Details - pgTransactionId: fetchedTransaction.pgTransactionId, - transactionOrderNumber: fetchedTransaction.transactionOrderNumber, - - //Payer Name should be persisted in transacation Table so that it can be used for DocRegen - - consolidatedPaymentMethod: ( - await getPaymentCodeFromCache( - this.cacheManager, - fetchedTransaction.paymentMethodTypeCode, - fetchedTransaction.paymentCardTypeCode, - ) - ).consolidatedPaymentMethod, - transactionDate: convertUtcToPt( - fetchedTransaction.transactionSubmitDate, - 'MMM. D, YYYY, hh:mm a Z', - ), - }, - }; - - const generatedReceiptDocumentPromise = this.dopsService.generateDocument( - currentUser, - dopsRequestData, - companyInfo?.companyId, - ); - - const generatedDocuments: ReadFileDto[] = await Promise.all([ - generatedPermitDocumentPromise, - generatedReceiptDocumentPromise, - ]); - - // Update Document Id on new permit - await queryRunner.manager.update( - Permit, - { - permitId: newPermit.permitId, - }, - { - documentId: generatedDocuments.at(0).documentId, - updatedDateTime: new Date(), - updatedUser: currentUser.userName, - updatedUserDirectory: currentUser.orbcUserDirectory, - updatedUserGuid: currentUser.userGUID, - }, - ); - - // Update Document Id on new receipt - await queryRunner.manager.update( - Receipt, - { - receiptId: fetchedTransaction.receipt.receiptId, - }, - { - receiptDocumentId: generatedDocuments.at(1).documentId, - updatedDateTime: new Date(), - updatedUser: currentUser.userName, - updatedUserDirectory: currentUser.orbcUserDirectory, - updatedUserGuid: currentUser.userGUID, - }, - ); - await queryRunner.commitTransaction(); - success = permit.permitId; - - const emailList = [ - permitDataForTemplate.permitData?.contactDetails?.email, - permitDataForTemplate.permitData?.contactDetails?.additionalEmail, - voidPermitDto.additionalEmail, - companyInfo.email, - ].filter((email) => Boolean(email)); - - const distinctEmailList = Array.from(new Set(emailList)); - - let notificationDocument: INotificationDocument = { - templateName: NotificationTemplate.ISSUE_PERMIT, - to: distinctEmailList, - subject: 'onRouteBC Permits - ' + companyInfo.legalName, - documentIds: [generatedDocuments?.at(0)?.documentId], - }; - - void this.dopsService.notificationWithDocumentsFromDops( - currentUser, - notificationDocument, - true, - ); - - notificationDocument = { - templateName: NotificationTemplate.PAYMENT_RECEIPT, - to: distinctEmailList, - subject: `onRouteBC Permit Receipt - ${fetchedTransaction?.receipt?.receiptNumber}`, - documentIds: [generatedDocuments?.at(1)?.documentId], - }; - - void this.dopsService.notificationWithDocumentsFromDops( - currentUser, - notificationDocument, - true, - ); + success = permitId; + voidRevokedPermitId = newPermit.permitId; } catch (error) { await queryRunner.rollbackTransaction(); this.logger.error(error); @@ -831,8 +636,9 @@ export class PermitService { success: [success], failure: [failure], }; - return resultDto; + return { result: resultDto, voidRevokedPermitId }; } + /** * Retrieves permit type information from cache. * @returns A Promise resolving to a map of permit types. @@ -856,12 +662,12 @@ export class PermitService { } /** - * 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. + * Sends a notification associated with a permit, sending document(s) based on permit details and transactions. + * This includes handling permit details retrieval, and constructing the 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. + * @param createNotificationDto DTO specifying notification recipients and type. * @returns The result of the notification sending operation wrapped in a Promise. */ @LogAsyncMethodExecution() @@ -869,29 +675,19 @@ export class PermitService { currentUser: IUserJWT, permitId: string, createNotificationDto: CreateNotificationDto, - ): Promise { - let permitDocumentId: string; - let receiptDocumentId: string; + ): Promise { // 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') + .andWhere('receipt.receiptNumber IS NOT NULL') .getOne(); /** @@ -901,150 +697,56 @@ export class PermitService { const companyInfo = permit.company; - permitDocumentId = permit?.documentId; - receiptDocumentId = - permit?.permitTransactions?.at(0)?.transaction?.receipt - ?.receiptDocumentId; + const permitDocumentId = permit?.documentId; - //If permit Document or receipt is not attached to the permit - if (!permitDocumentId || !receiptDocumentId) { - const fullNames = await fetchPermitDataDescriptionValuesFromCache( - this.cacheManager, - permit, - ); + const receipt = permit?.permitTransactions?.at(0)?.transaction?.receipt; - const revisionHistory = await this.permitRepository.find({ - where: [ - { - originalPermitId: permit.originalPermitId, - permitId: LessThanOrEqual(permit.permitId), - }, - ], - order: { permitId: 'DESC' }, - }); + const readNotificationDtoList: ReadNotificationDto[] = []; + let notificationDocument: INotificationDocument; + if ( + createNotificationDto?.notificationType?.includes( + NotificationType.EMAIL_PERMIT, + ) + ) { + notificationDocument = { + templateName: NotificationTemplate.ISSUE_PERMIT, + to: createNotificationDto.to, + subject: `onRouteBC Permits - ${companyInfo.legalName}`, + documentIds: [permitDocumentId], + }; - 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( + // Send the constructed notification via the DOPS service and return the result + const readNotificationDto = + await this.dopsService.notificationWithDocumentsFromDops( currentUser, - dopsRequestData, - companyInfo.companyId, + notificationDocument, ); + readNotificationDto.notificationType = NotificationType.EMAIL_PERMIT; + readNotificationDtoList.push(readNotificationDto); + } - 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; + if ( + createNotificationDto?.notificationType?.includes( + NotificationType.EMAIL_RECEIPT, + ) + ) { + notificationDocument = { + templateName: NotificationTemplate.PAYMENT_RECEIPT, + to: createNotificationDto.to, + subject: `onRouteBC Permit Receipt - ${receipt?.receiptNumber}`, + documentIds: [receipt?.receiptDocumentId], + }; - await this.paymentService.updateReceiptDocument( + // Send the constructed notification via the DOPS service and return the result + const readNotificationDto = + await this.dopsService.notificationWithDocumentsFromDops( currentUser, - permit?.permitTransactions[0]?.transaction?.receipt?.receiptId, - receiptDocumentId, + notificationDocument, ); - } + readNotificationDto.notificationType = NotificationType.EMAIL_RECEIPT; + readNotificationDtoList.push(readNotificationDto); } - // 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, - documentIds: [permitDocumentId, receiptDocumentId], - }; - - // Send the constructed notification via the DOPS service and return the result - return await this.dopsService.notificationWithDocumentsFromDops( - currentUser, - notificationDocument, - ); + return readNotificationDtoList; } }