Skip to content

Commit 8041d5b

Browse files
authored
Merge pull request #92 from uwblueprint/INTF25-mutator-update-reviewed-applicant-record
[INTF25] Build Mutator to Update Reviewed Applicant Record
2 parents e684b4a + 42170f9 commit 8041d5b

File tree

6 files changed

+156
-0
lines changed

6 files changed

+156
-0
lines changed

backend/typescript/graphql/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const executableSchema = makeExecutableSchema({
5151
adminCommentType,
5252
applicantRecordType,
5353
reviewPageType,
54+
reviewedApplicantRecordTypes,
5455
],
5556
resolvers: merge(
5657
authResolvers,
@@ -62,6 +63,7 @@ const executableSchema = makeExecutableSchema({
6263
adminCommentResolvers,
6364
applicantRecordResolvers,
6465
reviewPageResolvers,
66+
reviewedApplicantRecordResolvers,
6567
),
6668
});
6769

@@ -93,6 +95,7 @@ const graphQLMiddlewares = {
9395
bulkCreateReviewedApplicantRecord: authorizedByAllRoles(),
9496
deleteReviewedApplicantRecord: authorizedByAllRoles(),
9597
bulkDeleteReviewedApplicantRecord: authorizedByAllRoles(),
98+
updateReviewedApplicantRecord: authorizedByAllRoles(),
9699
createUser: authorizedByAdmin(),
97100
updateUser: authorizedByAdmin(),
98101
deleteUserById: authorizedByAdmin(),

backend/typescript/graphql/resolvers/reviewedApplicantRecordResolver.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ReviewedApplicantRecordDTO,
44
CreateReviewedApplicantRecordDTO,
55
DeleteReviewedApplicantRecordDTO,
6+
UpdateReviewedApplicantRecordDTO,
67
} from "../../types";
78
import { getErrorMessage } from "../../utilities/errorUtils";
89

@@ -61,6 +62,19 @@ const reviewedApplicantRecordResolvers = {
6162
throw new Error(getErrorMessage(error));
6263
}
6364
},
65+
66+
updateReviewedApplicantRecord: async (
67+
_parent: undefined,
68+
args: { input: UpdateReviewedApplicantRecordDTO },
69+
): Promise<ReviewedApplicantRecordDTO> => {
70+
try {
71+
return await reviewedApplicantRecordService.updateReviewedApplicantRecord(
72+
args.input,
73+
);
74+
} catch (error) {
75+
throw new Error(getErrorMessage(error));
76+
}
77+
},
6478
},
6579
};
6680

backend/typescript/graphql/types/reviewedApplicantRecordTypes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ const reviewedApplicantRecordTypes = gql`
4545
reviewerId: Int!
4646
}
4747
48+
input UpdateReviewedApplicantRecordInput {
49+
applicantRecordId: ID!
50+
reviewerId: Int!
51+
review: ReviewInput
52+
status: String
53+
}
54+
4855
extend type Mutation {
4956
createReviewedApplicantRecord(
5057
input: CreateReviewedApplicantRecordInput!
@@ -61,6 +68,10 @@ const reviewedApplicantRecordTypes = gql`
6168
bulkDeleteReviewedApplicantRecord(
6269
inputs: [DeleteReviewedApplicantRecord!]!
6370
): [ReviewedApplicantRecord!]!
71+
72+
updateReviewedApplicantRecord(
73+
input: UpdateReviewedApplicantRecordInput!
74+
): ReviewedApplicantRecord!
6475
}
6576
`;
6677

backend/typescript/services/implementations/reviewedApplicantRecordService.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,45 @@
11
import { sequelize } from "../../models";
22
import ReviewedApplicantRecord from "../../models/reviewedApplicantRecord.model";
3+
import ApplicantRecord from "../../models/applicantRecord.model";
34
import {
45
ReviewedApplicantRecordDTO,
56
CreateReviewedApplicantRecordDTO,
67
DeleteReviewedApplicantRecordDTO,
8+
UpdateReviewedApplicantRecordDTO,
9+
Review,
710
} from "../../types";
811
import { getErrorMessage } from "../../utilities/errorUtils";
912
import logger from "../../utilities/logger";
1013
import IReviewApplicantRecordService from "../interfaces/IReviewedApplicantRecordService";
1114

1215
const Logger = logger(__filename);
1316

17+
function validateReviewScores(review: Review | undefined): void {
18+
if (!review) return;
19+
20+
const scores = {
21+
passionFSG: review.passionFSG,
22+
teamPlayer: review.teamPlayer,
23+
desireToLearn: review.desireToLearn,
24+
skill: review.skill,
25+
};
26+
27+
Object.entries(scores).forEach(([field, value]) => {
28+
if (value !== undefined && (value < 1 || value > 5)) {
29+
throw new Error(
30+
`Invalid score for ${field}: ${value}. Scores must be between 1 and 5.`,
31+
);
32+
}
33+
});
34+
}
35+
1436
class ReviewedApplicantRecordService implements IReviewApplicantRecordService {
1537
/* eslint-disable class-methods-use-this */
1638
async createReviewedApplicantRecord(
1739
dto: CreateReviewedApplicantRecordDTO,
1840
): Promise<ReviewedApplicantRecordDTO> {
1941
try {
42+
validateReviewScores(dto.review);
2043
const record = await ReviewedApplicantRecord.create(dto);
2144
return record.toJSON() as ReviewedApplicantRecordDTO;
2245
} catch (error: unknown) {
@@ -33,6 +56,10 @@ class ReviewedApplicantRecordService implements IReviewApplicantRecordService {
3356
createReviewedApplicantRecordDTOs: CreateReviewedApplicantRecordDTO[],
3457
): Promise<ReviewedApplicantRecordDTO[]> {
3558
try {
59+
createReviewedApplicantRecordDTOs.forEach((dto) => {
60+
validateReviewScores(dto.review);
61+
});
62+
3663
const reviewedApplicantRecords = await sequelize.transaction(
3764
async (t) => {
3865
const records = await ReviewedApplicantRecord.bulkCreate(
@@ -121,6 +148,90 @@ class ReviewedApplicantRecordService implements IReviewApplicantRecordService {
121148
throw error;
122149
}
123150
}
151+
152+
/* eslint-disable class-methods-use-this */
153+
async updateReviewedApplicantRecord({
154+
applicantRecordId,
155+
reviewerId,
156+
review,
157+
status,
158+
}: UpdateReviewedApplicantRecordDTO): Promise<ReviewedApplicantRecordDTO> {
159+
try {
160+
const updatedRecord = await sequelize.transaction(async (t) => {
161+
const reviewedRecord = await ReviewedApplicantRecord.findOne({
162+
where: { applicantRecordId, reviewerId },
163+
transaction: t,
164+
});
165+
166+
if (!reviewedRecord) {
167+
throw new Error(
168+
`ReviewedApplicantRecord not found for applicantRecordId: ${applicantRecordId} and reviewerId: ${reviewerId}`,
169+
);
170+
}
171+
172+
const oldReviewedScore = reviewedRecord.score || 0;
173+
174+
if (review !== undefined) {
175+
validateReviewScores(review);
176+
177+
reviewedRecord.review = {
178+
...reviewedRecord.review,
179+
...review,
180+
};
181+
182+
const { passionFSG, teamPlayer, desireToLearn, skill } =
183+
reviewedRecord.review;
184+
185+
let calculatedScore = 0;
186+
if (passionFSG !== undefined) calculatedScore += passionFSG;
187+
if (teamPlayer !== undefined) calculatedScore += teamPlayer;
188+
if (desireToLearn !== undefined) calculatedScore += desireToLearn;
189+
if (skill !== undefined) calculatedScore += skill;
190+
reviewedRecord.score = calculatedScore;
191+
192+
if (review.skillCategory !== undefined) {
193+
reviewedRecord.skillCategory = review.skillCategory;
194+
}
195+
}
196+
197+
if (status !== undefined) {
198+
reviewedRecord.status = status;
199+
}
200+
201+
await reviewedRecord.save({ transaction: t });
202+
203+
const newReviewedScore = reviewedRecord.score || 0;
204+
205+
const applicantRecord = await ApplicantRecord.findOne({
206+
where: { id: applicantRecordId },
207+
transaction: t,
208+
});
209+
210+
if (!applicantRecord) {
211+
throw new Error(
212+
`ApplicantRecord not found for applicantRecordId: ${applicantRecordId}`,
213+
);
214+
}
215+
216+
const oldCombinedScore = applicantRecord.combined_score || 0;
217+
applicantRecord.combined_score =
218+
oldCombinedScore - oldReviewedScore + newReviewedScore;
219+
220+
await applicantRecord.save({ transaction: t });
221+
222+
return reviewedRecord;
223+
});
224+
225+
return updatedRecord.toJSON() as ReviewedApplicantRecordDTO;
226+
} catch (error: unknown) {
227+
Logger.error(
228+
`Failed to update reviewed applicant record. Reason = ${getErrorMessage(
229+
error,
230+
)}`,
231+
);
232+
throw error;
233+
}
234+
}
124235
}
125236

126237
export default ReviewedApplicantRecordService;

backend/typescript/services/interfaces/IReviewedApplicantRecordService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
ReviewedApplicantRecordDTO,
33
CreateReviewedApplicantRecordDTO,
44
DeleteReviewedApplicantRecordDTO,
5+
UpdateReviewedApplicantRecordDTO,
56
} from "../../types";
67

78
interface IReviewApplicantRecordService {
@@ -37,6 +38,15 @@ interface IReviewApplicantRecordService {
3738
bulkDeleteReviewedApplicantRecord(
3839
deleteReviewedApplicantRecords: DeleteReviewedApplicantRecordDTO[],
3940
): Promise<ReviewedApplicantRecordDTO[]>;
41+
42+
/**
43+
* Updates the review content and/or status of a ReviewedApplicantRecord
44+
* Also updates the combined score in the ApplicantRecord table
45+
* @Param updateReviewedApplicantRecordDTO data to update reviewed applicant record
46+
*/
47+
updateReviewedApplicantRecord(
48+
updateReviewedApplicantRecordDTO: UpdateReviewedApplicantRecordDTO,
49+
): Promise<ReviewedApplicantRecordDTO>;
4050
}
4151

4252
export default IReviewApplicantRecordService;

backend/typescript/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,13 @@ export type DeleteReviewedApplicantRecordDTO = {
222222
reviewerId: number;
223223
};
224224

225+
export type UpdateReviewedApplicantRecordDTO = {
226+
applicantRecordId: string;
227+
reviewerId: number;
228+
review?: Review;
229+
status?: ReviewStatus;
230+
};
231+
225232
export type ReviewDetails = {
226233
reviewerFirstName: string;
227234
reviewerLastName: string;

0 commit comments

Comments
 (0)