From c98724d36a0037f7e6727558612466f3441d0087 Mon Sep 17 00:00:00 2001 From: Praveen Raju <80779423+praju-aot@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:59:29 -0500 Subject: [PATCH] ORV2-3283 - Get Metadata for application in queue (#1760) --- .../case-management.service.ts | 69 +++++++++++++++++++ .../dto/response/read-case-event.dto.ts | 8 +-- .../dto/response/read-case-meta.dto.ts | 49 +++++++++++++ .../profiles/case-management.profile.ts | 20 ++++++ .../dto/request/create-notification.dto.ts | 6 +- .../application/application.service.ts | 52 ++++++++++++++ .../company-application-queue.controller.ts | 46 +++++++++++++ 7 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 vehicles/src/modules/case-management/dto/response/read-case-meta.dto.ts diff --git a/vehicles/src/modules/case-management/case-management.service.ts b/vehicles/src/modules/case-management/case-management.service.ts index 3a51b8009..28e8eba56 100644 --- a/vehicles/src/modules/case-management/case-management.service.ts +++ b/vehicles/src/modules/case-management/case-management.service.ts @@ -24,6 +24,7 @@ import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; import { ReadCaseEvenDto } from './dto/response/read-case-event.dto'; import { ReadCaseActivityDto } from './dto/response/read-case-activity.dto'; +import { ReadCaseMetaDto } from './dto/response/read-case-meta.dto'; @Injectable() export class CaseManagementService { @@ -351,6 +352,74 @@ export class CaseManagementService { } } + /** + * Retrieves metadata for an existing case, ensuring it is available and in an acceptable state. + * If the case does not exist or is already closed, it throws appropriate exceptions. + * + * @param queryRunner - Optional, existing QueryRunner instance. + * @param caseId - Optional, the ID of the case to be queried. + * @param originalCaseId - Optional, the original ID of the case. + * @param applicationId - Optional, the ID of the permit associated with the case. + * @param existingCase - Optional, the existing `Case` entity to be queried. + * @returns A `Promise` object containing the metadata details of the case. + * @throws `DataNotFoundException` - If the case cannot be found or does not exist. + * @throws `UnprocessableEntityException` - If the case is already closed. + */ + @LogAsyncMethodExecution() + async getCaseMetadata({ + queryRunner, + caseId, + originalCaseId, + applicationId, + existingCase, + }: { + queryRunner?: Nullable; + caseId?: Nullable; + originalCaseId?: Nullable; + applicationId?: Nullable; + existingCase?: Nullable; + }): Promise { + let localQueryRunner = true; + ({ localQueryRunner, queryRunner } = await getQueryRunner({ + queryRunner, + dataSource: this.dataSource, + })); + try { + if (!existingCase) { + existingCase = await this.findLatest({ + queryRunner, + caseId, + originalCaseId, + applicationId, + }); + } + if (!existingCase) { + throw new DataNotFoundException(); + } else if (existingCase.caseStatusType === CaseStatusType.CLOSED) { + throwUnprocessableEntityException('Application no longer available.'); + } + + if (localQueryRunner) { + await queryRunner.commitTransaction(); + } + return await this.classMapper.mapAsync( + existingCase, + Case, + ReadCaseMetaDto, + ); + } catch (error) { + if (localQueryRunner) { + await queryRunner.rollbackTransaction(); + } + this.logger.error(error); + throw error; + } finally { + if (localQueryRunner) { + await queryRunner.release(); + } + } + } + /** * Starts the workflow for an existing case by changing its status to `IN_PROGRESS`. * It first retrieves or verifies the existence of the `Case` based on provided identifiers. diff --git a/vehicles/src/modules/case-management/dto/response/read-case-event.dto.ts b/vehicles/src/modules/case-management/dto/response/read-case-event.dto.ts index 2c6fe9bc2..e55306616 100644 --- a/vehicles/src/modules/case-management/dto/response/read-case-event.dto.ts +++ b/vehicles/src/modules/case-management/dto/response/read-case-event.dto.ts @@ -5,23 +5,21 @@ import { CaseEventType } from '../../../../common/enum/case-event-type.enum'; export class ReadCaseEvenDto { @AutoMap() @ApiProperty({ - description: - 'The unique identifier for the credit account status update activity linked to this user.', + description: 'The unique identifier of the the event linked to the case.', example: 4527, }) caseEventId: number; @AutoMap() @ApiProperty({ - description: 'The type of event that occurred in the credit account.', + description: 'The type of event that occurred.', example: CaseEventType.OPENED, }) caseEventType: CaseEventType; @AutoMap() @ApiProperty({ - description: - 'The date and time when the credit account activity took place.', + description: 'The date and time when the activity took place.', example: '2023-10-11T23:26:51.170Z', }) eventDate: string; diff --git a/vehicles/src/modules/case-management/dto/response/read-case-meta.dto.ts b/vehicles/src/modules/case-management/dto/response/read-case-meta.dto.ts new file mode 100644 index 000000000..10f2237c3 --- /dev/null +++ b/vehicles/src/modules/case-management/dto/response/read-case-meta.dto.ts @@ -0,0 +1,49 @@ +import { AutoMap } from '@automapper/classes'; +import { ApiProperty } from '@nestjs/swagger'; +import { CaseType } from '../../../../common/enum/case-type.enum'; +import { CaseStatusType } from '../../../../common/enum/case-status-type.enum'; + +export class ReadCaseMetaDto { + @AutoMap() + @ApiProperty({ + description: 'The unique identifier of the case.', + example: 4527, + }) + caseId: number; + + @AutoMap() + @ApiProperty({ + description: 'The type of case.', + example: CaseType.DEFAULT, + }) + caseType: CaseType; + + @AutoMap() + @ApiProperty({ + description: 'The Case Status type.', + example: CaseStatusType.OPEN, + }) + caseStatusType: CaseStatusType; + + @AutoMap() + @ApiProperty({ + description: 'The user name or id linked to the case.', + example: 'JSMITH', + }) + assignedUser: string; + + @AutoMap() + @ApiProperty({ + description: 'Id of the application.', + example: 74, + required: false, + }) + applicationId: string; + + @AutoMap() + @ApiProperty({ + example: 'A2-00000002-120', + description: 'Unique formatted permit application number.', + }) + applicationNumber: string; +} diff --git a/vehicles/src/modules/case-management/profiles/case-management.profile.ts b/vehicles/src/modules/case-management/profiles/case-management.profile.ts index 465818f12..f404c5d98 100644 --- a/vehicles/src/modules/case-management/profiles/case-management.profile.ts +++ b/vehicles/src/modules/case-management/profiles/case-management.profile.ts @@ -14,6 +14,8 @@ import { ReadCaseActivityDto } from '../dto/response/read-case-activity.dto'; import { IUserJWT } from '../../../common/interface/user-jwt.interface'; import { doesUserHaveRole } from '../../../common/helper/auth.helper'; import { IDIR_USER_ROLE_LIST } from '../../../common/enum/user-role.enum'; +import { Case } from '../entities/case.entity'; +import { ReadCaseMetaDto } from '../dto/response/read-case-meta.dto'; @Injectable() export class CaseManagementProfile extends AutomapperProfile { @@ -25,6 +27,24 @@ export class CaseManagementProfile extends AutomapperProfile { return (mapper: Mapper) => { createMap(mapper, CaseEvent, ReadCaseEvenDto); + createMap( + mapper, + Case, + ReadCaseMetaDto, + forMember( + (d) => d.assignedUser, + mapFrom((s) => s?.assignedUser?.userName), + ), + forMember( + (d) => d.applicationNumber, + mapFrom((s) => s?.permit?.applicationNumber), + ), + forMember( + (d) => d.applicationId, + mapFrom((s) => s?.permit?.permitId), + ), + ); + createMap( mapper, CaseActivity, 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 dd94c8727..cb0364f26 100644 --- a/vehicles/src/modules/common/dto/request/create-notification.dto.ts +++ b/vehicles/src/modules/common/dto/request/create-notification.dto.ts @@ -1,10 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { ApiProperty } from '@nestjs/swagger'; -import { - ArrayMinSize, - IsEmail, - IsEnum, -} from 'class-validator'; +import { ArrayMinSize, IsEmail, IsEnum } from 'class-validator'; import { NotificationType } from '../../../../common/enum/notification-type.enum'; export class CreateNotificationDto { 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 6a0bb3cc5..79dbda370 100644 --- a/vehicles/src/modules/permit-application-payment/application/application.service.ts +++ b/vehicles/src/modules/permit-application-payment/application/application.service.ts @@ -92,6 +92,7 @@ import { LoaDetail } from 'src/modules/special-auth/entities/loa-detail.entity'; import { getFromCache } from '../../../common/helper/cache.helper'; import { CacheKey } from '../../../common/enum/cache-key.enum'; import { FeatureFlagValue } from '../../../common/enum/feature-flag-value.enum'; +import { ReadCaseMetaDto } from '../../case-management/dto/response/read-case-meta.dto'; @Injectable() export class ApplicationService { @@ -609,12 +610,20 @@ export class ApplicationService { isPermitTypeEligibleForQueue(existingApplication.permitType) && existingApplication.permitStatus === ApplicationStatus.IN_QUEUE ) { + const existingCase = await this.caseManagementService.getCaseMetadata({ + applicationId, + }); + const permitData = JSON.parse( existingApplication?.permitData?.permitData, ) as PermitData; const currentDate = dayjs(new Date().toISOString())?.format('YYYY-MM-DD'); if (differenceBetween(permitData?.startDate, currentDate, 'days') > 0) { throwUnprocessableEntityException('Start Date is in the past.'); + } else if (existingCase.assignedUser !== currentUser.userName) { + throwUnprocessableEntityException( + `Application no longer available. This application is claimed by ${existingCase.assignedUser}`, + ); } } @@ -1244,6 +1253,49 @@ export class ApplicationService { return result; } + + /** + * Retrieves metadata for a case linked to an application in queue. + * Before fetching, the function ensures the application's status and type are valid for processing. + * + * Input: + * - @param currentUser: IUserJWT - The user performing the operation. + * - @param companyId: number - The ID of the company associated with the application. + * - @param applicationId: string - The ID of the application to be analyzed. + * + * Output: + * - @returns Promise - The metadata associated with the case of the application. + * + * Throws exceptions: + * - DataNotFoundException: When the application is not found. + * - UnprocessableEntityException: If the application is ineligible for queue. + * + */ + @LogAsyncMethodExecution() + async getCaseMetadata({ + companyId, + applicationId, + }: { + currentUser: IUserJWT; + companyId: number; + applicationId: string; + }): Promise { + const application = await this.findOne(applicationId, companyId); + if (!application) { + throw new DataNotFoundException(); + } else if (!isPermitTypeEligibleForQueue(application.permitType)) { + throwUnprocessableEntityException( + 'Invalid permit type. Ineligible for queue.', + ); + } else if (application.permitStatus !== ApplicationStatus.IN_QUEUE) { + throwUnprocessableEntityException('Invalid status.'); + } + const result = await this.caseManagementService.getCaseMetadata({ + applicationId, + }); + return result; + } + @LogAsyncMethodExecution() async createPermitLoa( currentUser: IUserJWT, diff --git a/vehicles/src/modules/permit-application-payment/application/company-application-queue.controller.ts b/vehicles/src/modules/permit-application-payment/application/company-application-queue.controller.ts index 10ce1ea06..a7dc09808 100644 --- a/vehicles/src/modules/permit-application-payment/application/company-application-queue.controller.ts +++ b/vehicles/src/modules/permit-application-payment/application/company-application-queue.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, ForbiddenException, + Get, Param, Post, Req, @@ -12,6 +13,7 @@ import { ApiInternalServerErrorResponse, ApiMethodNotAllowedResponse, ApiNotFoundResponse, + ApiOkResponse, ApiOperation, ApiTags, ApiUnprocessableEntityResponse, @@ -33,6 +35,7 @@ import { UpdateCaseActivity } from './dto/request/update-case-activity.dto'; import { ReadCaseEvenDto } from '../../case-management/dto/response/read-case-event.dto'; import { ApplicationIdIdPathParamDto } from './dto/request/pathParam/applicationId.path-params.dto'; import { IsFeatureFlagEnabled } from '../../../common/decorator/is-feature-flag-enabled.decorator'; +import { ReadCaseMetaDto } from '../../case-management/dto/response/read-case-meta.dto'; @ApiBearerAuth() @ApiTags('Company Application Queue') @IsFeatureFlagEnabled('APPLICATION-QUEUE') @@ -198,4 +201,47 @@ export class CompanyApplicationQueueController { return result; } + + /** + * Fetches metadata information for a case identified by the + * application ID. This method enforces authorization checks based on user roles. + * + * @param request - The incoming request containing user information. + * @param companyId - The ID of the company associated with the application. + * @param applicationId - The ID of the application for which the metadata + * information is to be fetched. + * @returns The case metadata information related to the application. + */ + @ApiOperation({ + summary: 'Fetch case metadata info for Application', + description: + `Returns the case metadata or throws exceptions if an error is encountered during the retrieval process. ` + + `Accessible only by ${IDIRUserRole.PPC_CLERK}, ${IDIRUserRole.SYSTEM_ADMINISTRATOR}, ${IDIRUserRole.CTPO}`, + }) + @ApiOkResponse({ + description: 'The retrieved case metadata.', + type: ReadCaseMetaDto, + }) + @Permissions({ + allowedIdirRoles: [ + IDIRUserRole.PPC_CLERK, + IDIRUserRole.SYSTEM_ADMINISTRATOR, + IDIRUserRole.CTPO, + ], + }) + @Get(':applicationId/queue/meta-data') + async getCaseMetadata( + @Req() request: Request, + @Param() { companyId, applicationId }: ApplicationIdIdPathParamDto, + ): Promise { + const currentUser = request.user as IUserJWT; + + const result = await this.applicationService.getCaseMetadata({ + currentUser, + companyId, + applicationId, + }); + + return result; + } }