Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ORV2-2973 - Application Queue resubmit and rejection #1592

Merged
merged 3 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ export class CaseActivityCommentConstraint
}
).caseActivityType; // Access the searchString property from the same object

// If CaseActivityType.APPROVED or CaseActivityType.REJECTED , comment should exists
if (caseActivityType !== CaseActivityType.WITHDRAWN && !comment) {
// If CaseActivityType.REJECTED, comment should exists
if (caseActivityType === CaseActivityType.REJECTED && !comment) {
return false;
}

return true;
}

defaultMessage() {
return `Comment is required when activity type is ${CaseActivityType.APPROVED} or ${CaseActivityType.REJECTED} `;
return `Comment is required when activity type is ${CaseActivityType.REJECTED} `;
}
}
97 changes: 64 additions & 33 deletions vehicles/src/modules/case-management/case-management.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { CaseNotes } from './entities/case-notes.entity';
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';

@Injectable()
export class CaseManagementService {
Expand All @@ -32,6 +33,8 @@ export class CaseManagementService {
private dataSource: DataSource,
@InjectRepository(Case)
private readonly caseRepository: Repository<Case>,
@InjectRepository(CaseActivity)
private readonly caseActivityRepository: Repository<CaseActivity>,
) {}

/**
Expand Down Expand Up @@ -126,9 +129,7 @@ export class CaseManagementService {
existingCase &&
existingCase?.caseStatusType !== CaseStatusType.CLOSED
) {
throwUnprocessableEntityException(
'An active case exists for the given application id',
);
throwUnprocessableEntityException('Application in queue already.');
}
let newCase = new Case();
newCase.caseType = CaseType.DEFAULT;
Expand Down Expand Up @@ -218,9 +219,7 @@ export class CaseManagementService {
if (!existingCase) {
throw new DataNotFoundException();
} else if (existingCase.caseStatusType === CaseStatusType.CLOSED) {
throwUnprocessableEntityException(
'Invalid status. Case is already closed',
);
throwUnprocessableEntityException('Application no longer available.');
}

existingCase.caseStatusType = CaseStatusType.CLOSED; //Rename to CaseStatusType
Expand Down Expand Up @@ -306,9 +305,7 @@ export class CaseManagementService {
if (!existingCase) {
throw new DataNotFoundException();
} else if (existingCase.caseStatusType === CaseStatusType.CLOSED) {
throwUnprocessableEntityException(
'Invalid status. Case is already closed',
);
throwUnprocessableEntityException('Application no longer available.');
}

existingCase.assignedUser = new User();
Expand Down Expand Up @@ -402,14 +399,8 @@ export class CaseManagementService {
}
if (!existingCase) {
throw new DataNotFoundException();
} else if (existingCase.caseStatusType === CaseStatusType.CLOSED) {
throwUnprocessableEntityException(
'Invalid status. Case is already closed',
);
} else if (existingCase.caseStatusType !== CaseStatusType.OPEN) {
throwUnprocessableEntityException(
'Cannot start workflow. Invalid status.',
);
throwUnprocessableEntityException('Application no longer available.');
}

existingCase.caseStatusType = CaseStatusType.IN_PROGRESS;
Expand Down Expand Up @@ -502,14 +493,12 @@ export class CaseManagementService {
}
if (!existingCase) {
throw new DataNotFoundException();
} else if (existingCase.caseStatusType === CaseStatusType.CLOSED) {
} else if (existingCase.assignedUser?.userGUID !== currentUser.userGUID) {
throwUnprocessableEntityException(
'Invalid status. Case is already closed',
`Application no longer available. This application is claimed by ${existingCase.assignedUser?.userName}`,
);
} else if (existingCase.caseStatusType !== CaseStatusType.IN_PROGRESS) {
throwUnprocessableEntityException(
'Cannot complete workflow. Invalid status.',
);
throwUnprocessableEntityException('Application no longer available.');
}

let caseNotes: CaseNotes;
Expand All @@ -534,6 +523,8 @@ export class CaseManagementService {
newActivity.caseEvent = newEvent;
newActivity.caseActivityType = caseActivityType;
newActivity.dateTime = new Date();
newActivity.user = new User();
newActivity.user.userGUID = currentUser.userGUID;
setBaseEntityProperties({ entity: newActivity, currentUser });
if (comment) {
newActivity.caseNotes = caseNotes;
Expand Down Expand Up @@ -616,13 +607,9 @@ export class CaseManagementService {
}
if (!existingCase) {
throw new DataNotFoundException();
} else if (existingCase.caseStatusType === CaseStatusType.CLOSED) {
throwUnprocessableEntityException(
'Invalid status. Case is already closed',
);
} else if (existingCase.caseStatusType === CaseStatusType.IN_PROGRESS) {
} else if (existingCase.caseStatusType !== CaseStatusType.OPEN) {
throwUnprocessableEntityException(
'Unable to withdraw the application in review',
'Application Status Application(s) have either been withdrawn or are in review by the Provincial Permit Centre.',
);
}

Expand All @@ -638,6 +625,8 @@ export class CaseManagementService {
newActivity.caseEvent = newEvent;
newActivity.caseActivityType = CaseActivityType.WITHDRAWN;
newActivity.dateTime = new Date();
newActivity.user = new User();
newActivity.user.userGUID = currentUser.userGUID;
setBaseEntityProperties({ entity: newActivity, currentUser });
await queryRunner.manager.save<CaseActivity>(newActivity);

Expand Down Expand Up @@ -720,13 +709,8 @@ export class CaseManagementService {
}
if (!existingCase) {
throw new DataNotFoundException();
}
if (existingCase.caseStatusType === CaseStatusType.CLOSED) {
throwUnprocessableEntityException(
'Invalid status. Case is already closed',
);
} else if (existingCase.caseStatusType !== CaseStatusType.IN_PROGRESS) {
throwUnprocessableEntityException('Cannot add notes. Invalid status.');
throwUnprocessableEntityException('Application no longer available.');
}
try {
let newEvent = this.createEvent(
Expand Down Expand Up @@ -833,4 +817,51 @@ export class CaseManagementService {
}
}
}

/**
* Retrieves the activity history for a specific case by fetching and mapping `CaseActivity` records.
* Filters are applied based on the permit's `applicationId` and the specified `caseActivityType`.
* Joins additional details, including user information and associated case notes, for each activity.
*
* @param currentUser - The current user executing the action.
* @param applicationId - The ID of the permit associated with the case.
* @param caseActivityType - The type of case activity to filter.
* @returns A `Promise<ReadCaseActivityDto[]>` containing the list of activities for the specified case.
*/
@LogAsyncMethodExecution()
async fetchActivityHistory({
currentUser,
applicationId,
caseActivityType,
}: {
currentUser: IUserJWT;
applicationId: Nullable<string>;
caseActivityType: CaseActivityType;
}): Promise<ReadCaseActivityDto[]> {
const caseActivity = await this.caseActivityRepository
.createQueryBuilder('caseActivity')
.innerJoinAndSelect('caseActivity.user', 'user')
.leftJoinAndSelect('caseActivity.caseNotes', 'caseNotes')
.innerJoinAndSelect('caseActivity.case', 'case')
.innerJoinAndSelect('case.permit', 'permit')
.where('permit.id = :applicationId', { applicationId })
.andWhere('caseActivity.caseActivityType = :caseActivityType', {
caseActivityType,
})
.orderBy('caseActivity.dateTime', 'DESC')
.getMany();

const caseActivityDto = await this.classMapper.mapArrayAsync(
caseActivity,
CaseActivity,
ReadCaseActivityDto,
{
extraArgs: () => ({
currentUser: currentUser,
}),
},
);

return caseActivityDto;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { AutoMap } from '@automapper/classes';
import { ApiProperty } from '@nestjs/swagger';
import { Nullable } from '../../../../common/types/common';

export class ReadCaseActivityDto {
@AutoMap()
@ApiProperty({
description: 'The unique case acitivty id.',
example: 1,
})
caseActivityId: number;

@AutoMap()
@ApiProperty({
description:
'The user name or id linked to the activity. This value is returned only when queried by a staff user.',
example: 'JSMITH',
required: false,
})
userName?: string;

@AutoMap()
@ApiProperty({
description: 'The date and time when the activity took place.',
example: '2023-10-11T23:26:51.170Z',
})
dateTime: string;

@AutoMap()
@ApiProperty({
description: 'The reason for activity.',
example: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
required: false,
type: 'string',
})
caseNotes?: Nullable<string>;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
import { Mapper, createMap } from '@automapper/core';
import {
Mapper,
createMap,
forMember,
mapFrom,
mapWithArguments,
} from '@automapper/core';
import { Injectable } from '@nestjs/common';
import { CaseEvent } from '../entities/case-event.entity';
import { ReadCaseEvenDto } from '../dto/response/read-case-event.dto';
import { CaseActivity } from '../entities/case-activity.entity';
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';

@Injectable()
export class CaseManagementProfile extends AutomapperProfile {
Expand All @@ -13,6 +24,28 @@ export class CaseManagementProfile extends AutomapperProfile {
override get profile() {
return (mapper: Mapper) => {
createMap(mapper, CaseEvent, ReadCaseEvenDto);

createMap(
mapper,
CaseActivity,
ReadCaseActivityDto,
forMember(
(d) => d.caseNotes,
mapFrom((s) => s?.caseNotes?.comment),
),
forMember(
(d) => d.userName,
mapWithArguments(
(source, { currentUser }: { currentUser: IUserJWT }) => {
if (
doesUserHaveRole(currentUser.orbcUserRole, IDIR_USER_ROLE_LIST)
) {
return source.user?.userName;
}
},
),
),
);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import { PermitData } from '../../../common/interface/permit.template.interface'
import { ApplicationApprovedNotification } from '../../../common/interface/application-approved.notification.interface';
import { ApplicationRejectedNotification } from '../../../common/interface/application-rejected.notification.interface';
import { convertUtcToPt } from '../../../common/helper/date-time.helper';
import { ReadCaseActivityDto } from '../../case-management/dto/response/read-case-activity.dto';

@Injectable()
export class ApplicationService {
Expand Down Expand Up @@ -270,13 +271,24 @@ export class ApplicationService {
companyId?: number,
): Promise<ReadApplicationDto> {
const application = await this.findOne(applicationId, companyId);
let readCaseActivityList: ReadCaseActivityDto[];
if (isPermitTypeEligibleForQueue(application?.permitType)) {
readCaseActivityList =
await this.caseManagementService.fetchActivityHistory({
applicationId,
currentUser,
caseActivityType: CaseActivityType.REJECTED,
});
}

const readPermitApplicationdto = await this.classMapper.mapAsync(
application,
Permit,
ReadApplicationDto,
{
extraArgs: () => ({
currentUserRole: currentUser?.orbcUserRole,
readCaseActivityList: readCaseActivityList,
}),
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ApplicationStatus } from 'src/common/enum/application-status.enum';
import { PermitApplicationOrigin } from 'src/common/enum/permit-application-origin.enum';
import { PermitApprovalSource } from 'src/common/enum/permit-approval-source.enum';
import { PermitType } from 'src/common/enum/permit-type.enum';
import { ReadCaseActivityDto } from '../../../../case-management/dto/response/read-case-activity.dto';

export class ReadApplicationDto {
@AutoMap()
Expand Down Expand Up @@ -128,4 +129,12 @@ export class ReadApplicationDto {
required: false,
})
applicant: string;

@AutoMap()
@ApiProperty({
description: 'Application rejection history',
type: [ReadCaseActivityDto],
required: false,
})
rejectionHistory?: ReadCaseActivityDto[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ApplicationQueueStatus,
CaseStatusType,
} from '../../../../common/enum/case-status-type.enum';
import { ReadCaseActivityDto } from '../../../case-management/dto/response/read-case-activity.dto';

@Injectable()
export class ApplicationProfile extends AutomapperProfile {
Expand Down Expand Up @@ -207,6 +208,21 @@ export class ApplicationProfile extends AutomapperProfile {
}
}),
),
forMember(
(d) => d.rejectionHistory,
mapWithArguments(
(
s,
{
readCaseActivityList,
}: { readCaseActivityList: ReadCaseActivityDto[] },
) => {
if (readCaseActivityList?.length) {
return readCaseActivityList;
}
},
),
),
);

createMap(
Expand Down
Loading