From 84c5625949851a5d1129fd5c6c592fe78318a0d0 Mon Sep 17 00:00:00 2001 From: Praveen Raju <80779423+praju-aot@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:35:13 -0400 Subject: [PATCH] ORV2-2792 Staff Queue Changes (#1603) --- .../application-search.constraint.ts | 13 +++--- .../constraint/query-param-list.constraint.ts | 44 +++++++++++++++++++ .../common/enum/application-status.enum.ts | 2 +- .../src/common/enum/case-status-type.enum.ts | 36 +++++++++++++++ .../src/common/helper/permit-fee.helper.ts | 6 +-- .../application/application.controller.ts | 11 +++-- .../application/application.service.ts | 29 ++++++------ .../company-application.controller.ts | 10 ++++- .../getApplication.query-params.dto.ts | 30 ++++++------- .../profile/application.profile.ts | 31 ++++++------- .../payment/payment.service.ts | 5 ++- 11 files changed, 153 insertions(+), 64 deletions(-) create mode 100644 vehicles/src/common/constraint/query-param-list.constraint.ts diff --git a/vehicles/src/common/constraint/application-search.constraint.ts b/vehicles/src/common/constraint/application-search.constraint.ts index 8f367018e..1618c8e08 100644 --- a/vehicles/src/common/constraint/application-search.constraint.ts +++ b/vehicles/src/common/constraint/application-search.constraint.ts @@ -5,24 +5,25 @@ import { } from 'class-validator'; import { Nullable } from '../types/common'; import { ApplicationSearch } from '../enum/application-search.enum'; +import { ApplicationQueueStatus } from '../enum/case-status-type.enum'; @ValidatorConstraint({ name: 'ApplicationSearch', async: false }) export class ApplicationSearchConstraint implements ValidatorConstraintInterface { validate( - value: ApplicationSearch | boolean | undefined, + value: ApplicationSearch | boolean | undefined | ApplicationQueueStatus[], args: ValidationArguments, ) { const fields = args.object as { pendingPermits?: Nullable; - applicationsInQueue?: Nullable; + applicationQueueStatus?: Nullable; searchColumn?: Nullable; searchString?: Nullable; }; if ( fields.pendingPermits != undefined && - fields.applicationsInQueue !== undefined + fields.applicationQueueStatus?.length ) { return false; } else if (fields.searchColumn && !fields.searchString) { @@ -35,16 +36,16 @@ export class ApplicationSearchConstraint const message: string[] = []; const fields = args.object as { pendingPermits?: Nullable; - applicationsInQueue?: Nullable; + applicationQueueStatus?: Nullable; searchColumn?: Nullable; searchString?: Nullable; }; if ( fields.pendingPermits != undefined && - fields.applicationsInQueue !== undefined + fields.applicationQueueStatus?.length ) { message.push( - 'Both pendingPermits and applicationsInQueue cannot be set at the same time.', + 'Both pendingPermits and applicationQueueStatus cannot be set at the same time.', ); } if (fields.searchColumn && !fields.searchString) { diff --git a/vehicles/src/common/constraint/query-param-list.constraint.ts b/vehicles/src/common/constraint/query-param-list.constraint.ts new file mode 100644 index 000000000..68d5f9f5c --- /dev/null +++ b/vehicles/src/common/constraint/query-param-list.constraint.ts @@ -0,0 +1,44 @@ +import { + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, +} from 'class-validator'; +import { Nullable } from '../types/common'; + +@ValidatorConstraint({ name: 'QueryParamList', async: false }) +export class QueryParamListConstraint implements ValidatorConstraintInterface { + validate(field: Nullable, args: ValidationArguments): boolean { + return !this.validateQueryParamList(field, args)?.length; + } + + validateQueryParamList( + field: Nullable, + args: ValidationArguments, + ): string[] { + const fieldList = field?.split(',') || []; + const allowedFieldValues = Object.values( + args.constraints?.at(0) as Record, + ); + + return fieldList.flatMap((fieldValue) => { + const errors: string[] = []; + if (!allowedFieldValues.includes(fieldValue)) { + errors.push( + `${fieldValue} is an invalid value. Possible values are: ${Object.values(allowedFieldValues).join(', ')}.`, + ); + } + return errors; + }); + } + + defaultMessage(args: ValidationArguments): string { + const applicationQueueStatus = ( + args.object as { applicationQueueStatus?: Nullable } + ).applicationQueueStatus; + const invalidFields = this.validateQueryParamList( + applicationQueueStatus, + args, + ); + return invalidFields.join(' '); + } +} diff --git a/vehicles/src/common/enum/application-status.enum.ts b/vehicles/src/common/enum/application-status.enum.ts index 7b5f5ea27..547905b96 100644 --- a/vehicles/src/common/enum/application-status.enum.ts +++ b/vehicles/src/common/enum/application-status.enum.ts @@ -36,7 +36,7 @@ export const ACTIVE_APPLICATION_STATUS: readonly ApplicationStatus[] = [ ]; /** - * Application statuses including Application In Progress (AIP) and Pending Permits/Applications + * Application statuses including Application In Progress (AIP), Pending Permits/Applications & IN_QUEUE */ export const ALL_APPLICATION_STATUS: readonly ApplicationStatus[] = [ ApplicationStatus.IN_PROGRESS, diff --git a/vehicles/src/common/enum/case-status-type.enum.ts b/vehicles/src/common/enum/case-status-type.enum.ts index 52e228044..f68f926e4 100644 --- a/vehicles/src/common/enum/case-status-type.enum.ts +++ b/vehicles/src/common/enum/case-status-type.enum.ts @@ -18,3 +18,39 @@ export type ApplicationQueueStatus = export const ACTIVE_APPLICATION_QUEUE_STATUS: readonly ApplicationQueueStatus[] = [ApplicationQueueStatus.PENDING_REVIEW, ApplicationQueueStatus.IN_REVIEW]; + +const statusMapping: Record = { + [ApplicationQueueStatus.PENDING_REVIEW]: CaseStatusType.OPEN, + [ApplicationQueueStatus.IN_REVIEW]: CaseStatusType.IN_PROGRESS, + [ApplicationQueueStatus.CLOSED]: CaseStatusType.CLOSED, +}; + +/** + * Converts an ApplicationQueueStatus to its corresponding CaseStatusType. + * + * @param status The ApplicationQueueStatus to convert. + * @returns The corresponding CaseStatusType. + */ +export const convertApplicationQueueStatus = ( + statuses: ApplicationQueueStatus[], +): CaseStatusType[] => { + return statuses?.map((status) => statusMapping[status]); +}; + +const reverseStatusMapping: Record = { + [CaseStatusType.OPEN]: ApplicationQueueStatus.PENDING_REVIEW, + [CaseStatusType.CLOSED]: ApplicationQueueStatus.CLOSED, + [CaseStatusType.IN_PROGRESS]: ApplicationQueueStatus.IN_REVIEW, +}; + +/** + * Converts an array of CaseStatusType values to their corresponding ApplicationQueueStatus values. + * + * @param statuses An array of CaseStatusType values to convert. + * @returns An array of ApplicationQueueStatus values + */ +export const convertCaseStatus = ( + statuses: CaseStatusType[], +): ApplicationQueueStatus[] => { + return statuses.map((status) => reverseStatusMapping[status]); +}; diff --git a/vehicles/src/common/helper/permit-fee.helper.ts b/vehicles/src/common/helper/permit-fee.helper.ts index a774b0473..835687f0c 100644 --- a/vehicles/src/common/helper/permit-fee.helper.ts +++ b/vehicles/src/common/helper/permit-fee.helper.ts @@ -170,10 +170,8 @@ export const currentPermitFee = ( return oldAmount === 0 ? 0 : -pricePerTerm * permitTerms; } // For non void new application (exclude amendment application), if no fee applies, set the price per term to 0 for new application - if ((isNoFee && oldAmount === undefined) || oldAmount === 0) - return 0; - if (oldAmount === undefined) - oldAmount = 0; + if ((isNoFee && oldAmount === undefined) || oldAmount === 0) return 0; + if (oldAmount === undefined) oldAmount = 0; // Calculate fee for non void permit. return oldAmount > 0 ? pricePerTerm * permitTerms - oldAmount 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 7258c89e0..490a75f78 100644 --- a/vehicles/src/modules/permit-application-payment/application/application.controller.ts +++ b/vehicles/src/modules/permit-application-payment/application/application.controller.ts @@ -28,6 +28,10 @@ import { IDIR_USER_ROLE_LIST } from '../../../common/enum/user-role.enum'; import { PaginationDto } from '../../../common/dto/paginate/pagination'; import { ReadApplicationMetadataDto } from './dto/response/read-application-metadata.dto'; import { GetApplicationQueryParamsDto } from './dto/request/queryParam/getApplication.query-params.dto'; +import { + ApplicationQueueStatus, + convertApplicationQueueStatus, +} from '../../../common/enum/case-status-type.enum'; @ApiBearerAuth() @ApiTags('Application : API accessible exclusively to staff users and SA.') @@ -66,11 +70,10 @@ export class ApplicationController { orderBy, searchColumn, searchString, - applicationsInQueue, + applicationQueueStatus, }: GetApplicationQueryParamsDto, ): Promise> { const currentUser = request.user as IUserJWT; - return await this.applicationService.findAllApplications({ page, take, @@ -78,7 +81,9 @@ export class ApplicationController { currentUser, searchColumn, searchString, - applicationsInQueue, + applicationQueueStatus: convertApplicationQueueStatus( + (applicationQueueStatus?.split(',') as ApplicationQueueStatus[]) || [], + ), }); } 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 4b5386a0e..933c9a226 100644 --- a/vehicles/src/modules/permit-application-payment/application/application.service.ts +++ b/vehicles/src/modules/permit-application-payment/application/application.service.ts @@ -296,7 +296,7 @@ export class ApplicationService { } /** - * Retrieves applications based on multiple optional filters including user GUID, company ID, pending permits status, applications in queue, and a search string. + * Retrieves applications based on multiple optional filters including user GUID, company ID, pending permits status, applications queue status, and a search string. * The function supports sorting by various columns and includes pagination for efficient retrieval. * @param findAllApplicationsOptions - Contains multiple optional parameters: pagination, sorting, filtering by company ID, user GUID, and other search filters. * - page: The current page number for pagination. @@ -306,7 +306,7 @@ export class ApplicationService { * - companyId: The ID of the company to filter applications by. * - userGUID: The GUID of the user whose applications to filter. * - currentUser: The current logged-in user's JWT payload. - * - applicationsInQueue: Boolean filter for applications that are in the queue. + * - applicationQueueStatus: Status filter for applications that are in the queue. * - searchColumn: The specific column to search within (e.g., plate, application number). * - searchString: The input keyword to use for searching. * @returns A paginated result containing filtered and sorted ReadApplicationMetadataDto objects. @@ -320,7 +320,7 @@ export class ApplicationService { companyId?: number; userGUID?: string; currentUser?: IUserJWT; - applicationsInQueue?: Nullable; + applicationQueueStatus?: Nullable; searchColumn?: Nullable; searchString?: Nullable; }): Promise> { @@ -332,7 +332,7 @@ export class ApplicationService { findAllApplicationsOptions.userGUID, findAllApplicationsOptions.searchColumn, findAllApplicationsOptions.searchString, - findAllApplicationsOptions.applicationsInQueue, + findAllApplicationsOptions.applicationQueueStatus, ); // total number of items const totalItems = await applicationsQB.getCount(); @@ -389,7 +389,8 @@ export class ApplicationService { currentUserRole: findAllApplicationsOptions?.currentUser?.orbcUserRole, currentDateTime: new Date(), - applicationsInQueue: findAllApplicationsOptions.applicationsInQueue, + applicationQueueStatus: + findAllApplicationsOptions.applicationQueueStatus, }), }, ); @@ -404,12 +405,12 @@ export class ApplicationService { userGUID?: string, searchColumn?: Nullable, searchString?: Nullable, - applicationsInQueue?: Nullable, + applicationQueueStatus?: Nullable, ): SelectQueryBuilder { - // Ensure that pendingPermits and applicationsInQueue are not set at the same time - if (pendingPermits !== undefined && applicationsInQueue !== undefined) { + // Ensure that pendingPermits and applicationQueueStatus are not set at the same time + if (pendingPermits !== undefined && applicationQueueStatus?.length) { throw new InternalServerErrorException( - 'Both pendingPermits and applicationsInQueue cannot be set at the same time.', + 'Both pendingPermits and applicationQueueStatus cannot be set at the same time.', ); } @@ -425,7 +426,7 @@ export class ApplicationService { ); // Include cases and the assigned case user only if applications are in queue - if (applicationsInQueue) { + if (applicationQueueStatus?.length) { permitsQuery = permitsQuery.innerJoinAndSelect('permit.cases', 'cases'); permitsQuery = permitsQuery.leftJoinAndSelect( 'cases.assignedUser', @@ -444,7 +445,7 @@ export class ApplicationService { } // Handle various status filters depending on the provided flags - if (applicationsInQueue) { + if (applicationQueueStatus?.length) { // If retrieving applications in queue, we filter those with "IN_QUEUE" status and open/in-progress cases permitsQuery = permitsQuery.andWhere( 'permit.permitStatus = :permitStatus', @@ -455,7 +456,7 @@ export class ApplicationService { permitsQuery = permitsQuery.andWhere( 'cases.caseStatusType IN (:...caseStatuses)', { - caseStatuses: [CaseStatusType.OPEN, CaseStatusType.IN_PROGRESS], + caseStatuses: applicationQueueStatus, }, ); } else if (pendingPermits) { @@ -467,7 +468,7 @@ export class ApplicationService { }); }), ); - } else if (pendingPermits === false || applicationsInQueue === false) { + } else if (pendingPermits === false) { // Filter active applications based on ACTIVE_APPLICATION_STATUS permitsQuery = permitsQuery.andWhere( new Brackets((qb) => { @@ -478,7 +479,7 @@ export class ApplicationService { ); } else if ( pendingPermits === undefined || - applicationsInQueue === undefined + !applicationQueueStatus?.length ) { // Filter all applications based on ALL_APPLICATION_STATUS permitsQuery = permitsQuery.andWhere( diff --git a/vehicles/src/modules/permit-application-payment/application/company-application.controller.ts b/vehicles/src/modules/permit-application-payment/application/company-application.controller.ts index cfdb8c294..0e674868b 100644 --- a/vehicles/src/modules/permit-application-payment/application/company-application.controller.ts +++ b/vehicles/src/modules/permit-application-payment/application/company-application.controller.ts @@ -48,6 +48,10 @@ import { ReadApplicationMetadataDto } from './dto/response/read-application-meta import { GetApplicationQueryParamsDto } from './dto/request/queryParam/getApplication.query-params.dto'; import { ApiPaginatedResponse } from 'src/common/decorator/api-paginate-response'; import { PermitReceiptDocumentService } from '../permit-receipt-document/permit-receipt-document.service'; +import { + ApplicationQueueStatus, + convertApplicationQueueStatus, +} from '../../../common/enum/case-status-type.enum'; @ApiBearerAuth() @ApiTags('Company Application') @@ -112,7 +116,11 @@ export class CompanyApplicationController { pendingPermits: getApplicationQueryParamsDto.pendingPermits, userGUID: userGuid, currentUser: currentUser, - applicationsInQueue: getApplicationQueryParamsDto.applicationsInQueue, + applicationQueueStatus: convertApplicationQueueStatus( + (getApplicationQueryParamsDto?.applicationQueueStatus?.split( + ',', + ) as ApplicationQueueStatus[]) || [], + ), searchColumn: getApplicationQueryParamsDto.searchColumn, searchString: getApplicationQueryParamsDto.searchString, }); diff --git a/vehicles/src/modules/permit-application-payment/application/dto/request/queryParam/getApplication.query-params.dto.ts b/vehicles/src/modules/permit-application-payment/application/dto/request/queryParam/getApplication.query-params.dto.ts index 33ef61b3e..5a3e90165 100644 --- a/vehicles/src/modules/permit-application-payment/application/dto/request/queryParam/getApplication.query-params.dto.ts +++ b/vehicles/src/modules/permit-application-payment/application/dto/request/queryParam/getApplication.query-params.dto.ts @@ -18,7 +18,8 @@ import { ALL_APPLICATION_STATUS, } from '../../../../../../common/enum/application-status.enum'; import { ApplicationSearchConstraint } from '../../../../../../common/constraint/application-search.constraint'; -import { ACTIVE_APPLICATION_QUEUE_STATUS } from '../../../../../../common/enum/case-status-type.enum'; +import { ApplicationQueueStatus } from '../../../../../../common/enum/case-status-type.enum'; +import { QueryParamListConstraint } from '../../../../../../common/constraint/query-param-list.constraint'; export class GetApplicationQueryParamsDto extends PageOptionsDto { @ApiProperty({ @@ -93,23 +94,20 @@ export class GetApplicationQueryParamsDto extends PageOptionsDto { pendingPermits?: Nullable; @ApiProperty({ + example: `${Object.values(ApplicationQueueStatus).join(',')}`, description: - `Setting this property true restricts the search results to those applications which are in queue (${Object.values(ACTIVE_APPLICATION_QUEUE_STATUS).join(', ')}). ` + - `Conversely, Setting it to false confines the search results to only those applications that are awaiting payment (${Object.values(ACTIVE_APPLICATION_STATUS).join(', ')}). ` + - `If left unspecified, the system will fetch all applications that are in any of the following statuses: ${Object.values(ALL_APPLICATION_STATUS).join(', ')}, including those awaiting issuance. ` + - 'Caution: You cannot set both pendingPermits and applicationsInQueue properties at the same time.', - example: true, + 'A string representing the application queue status order for query results. ' + + 'If unspecified, the results will not follow any specific order. ' + + 'The format involves a status code, with possible values being case-sensitive and must align with those defined in the schema. ' + + 'Start ordering using a status code, and optionally add more by joining them with commas. ' + + `Possible values are: ${Object.values(ApplicationQueueStatus).join(', ')}. ` + + 'Syntax: ', required: false, - type: 'boolean', + type: 'string', }) @IsOptional() - @Transform(({ obj, key }: { obj: Record; key: string }) => { - return obj[key] === 'true' ? true : obj[key] === 'false' ? false : obj[key]; - }) - @Validate(ApplicationSearchConstraint, { - message: - 'Both pendingPermits and applicationsInQueue cannot be set at the same time.', - }) - @IsBoolean() - applicationsInQueue?: Nullable; + @Validate(QueryParamListConstraint, [ApplicationQueueStatus]) + @IsString() + @Length(1, 150) + applicationQueueStatus?: Nullable; } diff --git a/vehicles/src/modules/permit-application-payment/application/profile/application.profile.ts b/vehicles/src/modules/permit-application-payment/application/profile/application.profile.ts index d85a45442..1f63de139 100644 --- a/vehicles/src/modules/permit-application-payment/application/profile/application.profile.ts +++ b/vehicles/src/modules/permit-application-payment/application/profile/application.profile.ts @@ -23,8 +23,8 @@ import { Permit } from '../../permit/entities/permit.entity'; import { differenceBetween } from '../../../../common/helper/date-time.helper'; import { Nullable } from '../../../../common/types/common'; import { - ApplicationQueueStatus, CaseStatusType, + convertCaseStatus, } from '../../../../common/enum/case-status-type.enum'; import { ReadCaseActivityDto } from '../../../case-management/dto/response/read-case-activity.dto'; @@ -289,18 +289,13 @@ export class ApplicationProfile extends AutomapperProfile { ( s, { - applicationsInQueue, - }: { applicationsInQueue?: Nullable }, + applicationQueueStatus, + }: { applicationQueueStatus?: Nullable }, ) => { - if (applicationsInQueue && s.cases?.length) { - switch (s.cases?.at(0)?.caseStatusType) { - case CaseStatusType.OPEN: - return ApplicationQueueStatus.PENDING_REVIEW; - case CaseStatusType.IN_PROGRESS: - return ApplicationQueueStatus.IN_REVIEW; - case CaseStatusType.CLOSED: - return ApplicationQueueStatus.CLOSED; - } + if (applicationQueueStatus?.length && s.cases?.length) { + return convertCaseStatus([s.cases?.at(0)?.caseStatusType])?.at( + 0, + ); } }, ), @@ -313,15 +308,15 @@ export class ApplicationProfile extends AutomapperProfile { { currentUserRole, currentDateTime, - applicationsInQueue, + applicationQueueStatus, }: { currentUserRole: UserRole; currentDateTime: Date; - applicationsInQueue?: Nullable; + applicationQueueStatus?: Nullable; }, ) => { if ( - applicationsInQueue && + applicationQueueStatus?.length && doesUserHaveRole(currentUserRole, IDIR_USER_ROLE_LIST) ) { const diff = differenceBetween( @@ -346,14 +341,14 @@ export class ApplicationProfile extends AutomapperProfile { s, { currentUserRole, - applicationsInQueue, + applicationQueueStatus, }: { currentUserRole: UserRole; - applicationsInQueue?: Nullable; + applicationQueueStatus?: Nullable; }, ) => { if ( - applicationsInQueue && + applicationQueueStatus?.length && doesUserHaveRole(currentUserRole, IDIR_USER_ROLE_LIST) && s.cases?.length ) { 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 d0af3097b..3c82633d9 100644 --- a/vehicles/src/modules/permit-application-payment/payment/payment.service.ts +++ b/vehicles/src/modules/permit-application-payment/payment/payment.service.ts @@ -788,7 +788,10 @@ export class PaymentService { application.company.companyId, queryRunner, ); - const oldAmount = permitPaymentHistory.length > 0?calculatePermitAmount(permitPaymentHistory):undefined; + const oldAmount = + permitPaymentHistory.length > 0 + ? calculatePermitAmount(permitPaymentHistory) + : undefined; const fee = permitFee(application, isNoFee, oldAmount); return fee; }