Skip to content
Open
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
162 changes: 113 additions & 49 deletions src/services/reviewFeedback.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import { getUser } from "./user.service";
import ReviewFeedback from "../models/ReviewFeedback";
import User from "../models/User";
import UserSettings from "../models/UserSettings";
import { IReviewFeedback, IUser, IReviewFeedbackOutput, CompleteFeedback } from "../types";

import {
IReviewFeedback,
IUser,
IReviewFeedbackOutput,
CompleteFeedback,
} from "../types";
import { addNotification } from "./notification.service";
import logger from "../config/loggingConfig";

/**
Expand All @@ -19,53 +24,66 @@ import logger from "../config/loggingConfig";
const computeRatings = async (user_settings_id: string) => {
try {
// Fetch all reviews for the user
const reviewFeedbackCount = await ReviewFeedback.countDocuments({ review_receiver_id: user_settings_id }).exec();
const reviewFeedbackCount = await ReviewFeedback.countDocuments({
review_receiver_id: user_settings_id,
}).exec();
if (reviewFeedbackCount === 0) {
// Default value when there are no reviews
await UserSettings.findOneAndUpdate({ user_settings_id }, { trust_meter_rating: 100 }).exec();
await UserSettings.findOneAndUpdate(
{ user_settings_id },
{ trust_meter_rating: 100 }
).exec();
return 100;
}
// Calculate the total number of reviews and the number of zero ratings
const totalReviews = reviewFeedbackCount
const zeroRatingsCount = await ReviewFeedback.countDocuments({ review_receiver_id: user_settings_id, rating: 0 }).exec();
const totalReviews = reviewFeedbackCount;
const zeroRatingsCount = await ReviewFeedback.countDocuments({
review_receiver_id: user_settings_id,
rating: 0,
}).exec();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW: You are calling ReviewFeedback.countDocuments twice (for totalReviews and zeroRatingsCount).
Consider a single aggregation to compute both in one round-trip for efficiency. You can try $group pipeline to compute totalReviews and zeroRatingsCount in one query and reduce latency.


// Calculate the percentage of zero ratings
const zeroRatingsPercentage = (zeroRatingsCount / totalReviews) * 100;

// Determine the value based on the percentage of zero ratings
let value;
switch (true) {
case (zeroRatingsPercentage <= 5):
case zeroRatingsPercentage <= 5:
value = 100;
break;
case (zeroRatingsPercentage > 5 && zeroRatingsPercentage <= 10):
case zeroRatingsPercentage > 5 && zeroRatingsPercentage <= 10:
value = 80;
break;
case (zeroRatingsPercentage > 10 && zeroRatingsPercentage <= 20):
case zeroRatingsPercentage > 10 && zeroRatingsPercentage <= 20:
value = 50;
break;
default:
value = 0;
}

// Update the user's rating value in the database
await UserSettings.findOneAndUpdate({ user_settings_id }, { trust_meter_rating: value });
await UserSettings.findOneAndUpdate(
{ user_settings_id },
{ trust_meter_rating: value }
);
return value;
} catch (error: any) {
logger.error(`Failed to compute ratings for userSettingsID ${ user_settings_id }: ${ error }`);
logger.error(
`Failed to compute ratings for userSettingsID ${user_settings_id}: ${error}`
);
throw error;
}
};

export const getReviewFeedback = async (
review_receiver_id: string,
searchQuery?: string
review_receiver_id: string,
searchQuery?: string
): Promise<CompleteFeedback | null> => {
try {
//condition to search by username
if (searchQuery && searchQuery.trim()) {
const user = await User.findOne({
pi_username: searchQuery
pi_username: searchQuery,
});
if (!user) {
return null;
Expand All @@ -74,24 +92,32 @@ export const getReviewFeedback = async (
}

const receivedFeedbackList = await ReviewFeedback.find({
review_receiver_id: review_receiver_id
}).sort({ review_date: -1 }).exec();
review_receiver_id: review_receiver_id,
})
.sort({ review_date: -1 })
.exec();

const givenFeedbackList = await ReviewFeedback.find({
review_giver_id: review_receiver_id
}).sort({ review_date: -1 }).exec();
review_giver_id: review_receiver_id,
})
.sort({ review_date: -1 })
.exec();

const updatedReceivedFeedbackList = await Promise.all(
receivedFeedbackList.map(async (reviewFeedback) => {
// Retrieve user details for both giver and receiver
const reviewer = await getUser(reviewFeedback.review_giver_id);
const receiver = await getUser(reviewFeedback.review_receiver_id);

const giverName = reviewer ? reviewer.user_name : '';
const receiverName = receiver ? receiver.user_name : '';
const giverName = reviewer ? reviewer.user_name : "";
const receiverName = receiver ? receiver.user_name : "";

// Return the updated review feedback object
return { ...reviewFeedback.toObject(), giver: giverName, receiver: receiverName };
return {
...reviewFeedback.toObject(),
giver: giverName,
receiver: receiverName,
};
})
);

Expand All @@ -101,25 +127,32 @@ export const getReviewFeedback = async (
const reviewer = await getUser(reviewFeedback.review_giver_id);
const receiver = await getUser(reviewFeedback.review_receiver_id);

const giverName = reviewer ? reviewer.user_name : '';
const receiverName = receiver ? receiver.user_name : '';
const giverName = reviewer ? reviewer.user_name : "";
const receiverName = receiver ? receiver.user_name : "";

// Return the updated review feedback object
return { ...reviewFeedback.toObject(), giver: giverName, receiver: receiverName };
return {
...reviewFeedback.toObject(),
giver: giverName,
receiver: receiverName,
};
})
);
return {
givenReviews: updatedGivenFeedbackList,
receivedReviews: updatedReceivedFeedbackList
receivedReviews: updatedReceivedFeedbackList,
} as unknown as CompleteFeedback;

} catch (error: any) {
logger.error(`Failed to retrieve reviews for reviewReceiverID ${ review_receiver_id }: ${ error }`);
logger.error(
`Failed to retrieve reviews for reviewReceiverID ${review_receiver_id}: ${error}`
);
throw error;
}
};

export const getReviewFeedbackById = async (review_id: string): Promise<{
export const getReviewFeedbackById = async (
review_id: string
): Promise<{
review: IReviewFeedbackOutput | null;
replies: IReviewFeedbackOutput[];
} | null> => {
Expand All @@ -133,7 +166,9 @@ export const getReviewFeedbackById = async (review_id: string): Promise<{
}

// Fetch replies to the main review
const replies = await ReviewFeedback.find({ reply_to_review_id: review_id }).exec();
const replies = await ReviewFeedback.find({
reply_to_review_id: review_id,
}).exec();

// Fetch giver and receiver names for each reply asynchronously
const updatedReplyList = await Promise.all(
Expand All @@ -143,11 +178,15 @@ export const getReviewFeedbackById = async (review_id: string): Promise<{
getUser(reply.review_receiver_id),
]);

const giverName = reviewer?.user_name || 'Unknown';
const receiverName = receiver?.user_name || 'Unknown';
const giverName = reviewer?.user_name || "Unknown";
const receiverName = receiver?.user_name || "Unknown";

// Return updated reply object
return { ...reply.toObject(), giver: giverName, receiver: receiverName };
return {
...reply.toObject(),
giver: giverName,
receiver: receiverName,
};
})
);

Expand All @@ -157,42 +196,61 @@ export const getReviewFeedbackById = async (review_id: string): Promise<{
getUser(reviewFeedback.review_receiver_id),
]);

const giverName = reviewer?.user_name || 'Unknown';
const receiverName = receiver?.user_name || 'Unknown';
const giverName = reviewer?.user_name || "Unknown";
const receiverName = receiver?.user_name || "Unknown";

// Create the main review object with giver and receiver names
const mainReview = { ...reviewFeedback.toObject(), giver: giverName, receiver: receiverName };
const mainReview = {
...reviewFeedback.toObject(),
giver: giverName,
receiver: receiverName,
};

return {
review: mainReview as unknown as IReviewFeedbackOutput,
replies: updatedReplyList as unknown as IReviewFeedbackOutput[],
};
} catch (error: any) {
logger.error(`Failed to retrieve review for reviewID ${ review_id }: ${ error }`);
logger.error(
`Failed to retrieve review for reviewID ${review_id}: ${error}`
);
throw error;
}
};

export const addReviewFeedback = async (authUser: IUser, formData: any, image: string): Promise<IReviewFeedback> => {
export const addReviewFeedback = async (
authUser: IUser,
formData: any,
image: string
): Promise<IReviewFeedback> => {
try {
const reviewFeedbackData: Partial<IReviewFeedback> = {
review_receiver_id: formData.review_receiver_id || '',
review_receiver_id: formData.review_receiver_id || "",
review_giver_id: authUser.pi_uid,
reply_to_review_id: formData.reply_to_review_id || null,
rating: formData.rating || '',
comment: formData.comment || '',
image: image || '',
review_date: new Date()
rating: formData.rating || "",
comment: formData.comment || "",
image: image || "",
review_date: new Date(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW: Validate rating to number and validate range(if applicable). Reject/throw if invalid.
Ensure receiver id is present and non-empty. Throw error if not present/valid.
Cap comment length (e.g., 512/1024) to avoid abuse.
(Frontend also, try to sanitize comment to prevent injecting malicious js code thru user comments)

Note: These review inputs are applicable to updateReviewFeedback() also.

};
const newReviewFeedback = new ReviewFeedback(reviewFeedbackData);
const savedReviewFeedback = await newReviewFeedback.save();

const computedValue = await computeRatings(savedReviewFeedback.review_receiver_id);
const computedValue = await computeRatings(
savedReviewFeedback.review_receiver_id
);
logger.info(`Computed review rating: ${computedValue}`);

const message = `${
authUser.user_name || authUser.pi_uid
} has given you a review on ${new Date().toLocaleString()}`;
await addNotification(savedReviewFeedback.review_receiver_id, message);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW: Use ISO timestamp and avoid using toLocaleString() in backend. Clients can use toLocaleString.
Consider i18n for notification message (if its part of scope)

logger.info(
`Notification sent to ${savedReviewFeedback.review_receiver_id}: ${message}`
);
return savedReviewFeedback as IReviewFeedback;
} catch (error: any) {
logger.error(`Failed to add review: ${ error }`);
logger.error(`Failed to add review: ${error}`);
throw error;
}
};
Expand All @@ -206,15 +264,19 @@ export const updateReviewFeedback = async (
const review = await ReviewFeedback.findById(reviewId).exec();

if (!review) {
logger.error(`Review with ID ${ reviewId } not found`);
logger.error(`Review with ID ${reviewId} not found`);
const error = new Error(`Review with ID ${reviewId} not found`);
error.name = "NotFoundError";
throw error;
}

if (review.review_giver_id !== authUser.pi_uid) {
logger.error(`User with ID: ${ review.review_giver_id } does not have permission to update this review`);
const error = new Error(`User with ID: ${ review.review_giver_id } does not have permission to update this review`);
logger.error(
`User with ID: ${review.review_giver_id} does not have permission to update this review`
);
const error = new Error(
`User with ID: ${review.review_giver_id} does not have permission to update this review`
);
error.name = "ForbiddenError";
throw error;
}
Expand All @@ -232,6 +294,8 @@ export const updateReviewFeedback = async (

await computeRatings(review.review_receiver_id);

logger.info(`Review ${reviewId} updated successfully by user ${authUser.pi_uid}`);
logger.info(
`Review ${reviewId} updated successfully by user ${authUser.pi_uid}`
);
return review as IReviewFeedback;
};
};