From bb8de019d306321df21b5673ba188070e1a8bb5d Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 18 Oct 2025 12:09:34 -0400 Subject: [PATCH 1/6] updated the membership controller to handle the deduction of the 100 mappi tokens --- src/controllers/membershipController.ts | 102 ++++++++++++++++++++---- 1 file changed, 88 insertions(+), 14 deletions(-) diff --git a/src/controllers/membershipController.ts b/src/controllers/membershipController.ts index 29eec25..3170e8f 100644 --- a/src/controllers/membershipController.ts +++ b/src/controllers/membershipController.ts @@ -1,7 +1,9 @@ import { Request, Response } from "express"; import * as membershipService from "../services/membership.service"; -import { IUser } from "../types" +import { IUser } from "../types"; import logger from "../config/loggingConfig"; +import { addNotification } from "../services/notification.service"; +import { computeRatings } from "../services/reviewFeedback.service"; export const getMembershipList = async (req: Request, res: Response) => { try { @@ -14,14 +16,19 @@ export const getMembershipList = async (req: Request, res: Response) => { return res.status(200).json(membershipList); } catch (error) { logger.error(`Error getting membership list: `, error); - return res.status(500).json({ message: 'An error occurred while getting membership list; please try again later' }); + return res.status(500).json({ + message: + "An error occurred while getting membership list; please try again later", + }); } -}; +}; export const getSingleMembership = async (req: Request, res: Response) => { const { membership_id } = req.params; try { - const membership = await membershipService.getSingleMembershipById(membership_id); + const membership = await membershipService.getSingleMembershipById( + membership_id + ); if (!membership) { logger.warn(`Membership with ID ${membership_id} not found.`); return res.status(404).json({ message: "Membership not found" }); @@ -30,14 +37,19 @@ export const getSingleMembership = async (req: Request, res: Response) => { return res.status(200).json(membership); } catch (error) { logger.error(`Error getting membership ID ${membership_id}:`, error); - return res.status(500).json({ message: 'An error occurred while getting single membership; please try again later' }); + return res.status(500).json({ + message: + "An error occurred while getting single membership; please try again later", + }); } }; export const fetchUserMembership = async (req: Request, res: Response) => { const authUser = req.currentUser as IUser; try { - const currentMembership = await membershipService.getUserMembership(authUser); + const currentMembership = await membershipService.getUserMembership( + authUser + ); if (!currentMembership) { logger.warn(`User Membership with ID ${authUser.pi_uid} not found.`); return res.status(404).json({ message: "User Membership not found" }); @@ -45,24 +57,86 @@ export const fetchUserMembership = async (req: Request, res: Response) => { logger.info(`Fetched user membership with ID ${authUser.pi_uid}`); return res.status(200).json(currentMembership); } catch (error) { - logger.error(`Failed to fetch user membership with ID ${authUser.pi_uid}:`, error); - return res.status(500).json({ message: 'An error occurred while fetching user membership; please try again later' }); + logger.error( + `Failed to fetch user membership with ID ${authUser.pi_uid}:`, + error + ); + return res.status(500).json({ + message: + "An error occurred while fetching user membership; please try again later", + }); } }; -export const updateMembership = async (req: Request, res: Response) => { +export const updateMembership = async (req: Request, res: Response) => { try { const { membership_class } = req.body; const authUser = req.currentUser; if (!authUser) { - logger.warn('No authenticated user found when updating/ renewing membership.'); - return res.status(401).json({ error: 'Unauthorized' }); + logger.warn( + "No authenticated user found when updating/ renewing membership." + ); + return res.status(401).json({ error: "Unauthorized" }); } logger.info(`Updated or renewed membership for user ${authUser.pi_uid}`); - const updatedMembership = await membershipService.applyMembershipChange(authUser.pi_uid, membership_class); + const updatedMembership = await membershipService.applyMembershipChange( + authUser.pi_uid, + membership_class + ); return res.status(200).json(updatedMembership); } catch (error: any) { logger.error("Failed to update or renew membership:", error); - return res.status(500).json({ message: 'An error occurred while updating membership; please try again later' }); + return res.status(500).json({ + message: + "An error occurred while updating membership; please try again later", + }); + } +}; + +// Deduct Mappi, update trust rating, and send notification +export const deductMappi = async (req: Request, res: Response) => { + try { + const authUser = req.currentUser as IUser; + if (!authUser) { + logger.warn("No authenticated user found for Mappi deduction."); + return res.status(401).json({ message: "Unauthorized" }); + } + + const { amount } = req.body; + if (!amount || amount <= 0) { + return res.status(400).json({ message: "Invalid Mappi amount" }); + } + + // Deduct Mappi using your service + const deductionResult = await membershipService.deductMappi( + authUser.pi_uid, + amount + ); + + // Compute updated trust rating + const trustRating = await computeRatings(authUser.pi_uid); + + // Send notification to user + await addNotification( + authUser.pi_uid, + `You have successfully used ${amount} Mappi. Current balance: ${deductionResult.balance}` + ); + + logger.info(`Deducted ${amount} Mappi for user ${authUser.pi_uid}`); + + return res.status(200).json({ + message: deductionResult.message, + balance: deductionResult.balance, + mappi_used_to_date: deductionResult.mappi_used_to_date, + trust_meter_rating: trustRating, + }); + } catch (error: any) { + logger.error( + `Failed to deduct Mappi for user ${req.currentUser?.pi_uid}:`, + error + ); + return res + .status(500) + .json({ message: "Failed to deduct Mappi; please try again later" }); } -}; \ No newline at end of file +}; From 12ea2c06144091858c0dd58889b7a6ddbf04cfb1 Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 18 Oct 2025 12:11:11 -0400 Subject: [PATCH 2/6] Update the review controller to handle and allowthe update of the rating --- src/controllers/reviewFeedbackController.ts | 38 ++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/controllers/reviewFeedbackController.ts b/src/controllers/reviewFeedbackController.ts index 9d56e2d..8dec61c 100644 --- a/src/controllers/reviewFeedbackController.ts +++ b/src/controllers/reviewFeedbackController.ts @@ -4,6 +4,8 @@ import * as reviewFeedbackService from "../services/reviewFeedback.service"; import { uploadImage } from "../services/misc/image.service"; import logger from "../config/loggingConfig"; +import * as notificationService from "../services/notification.service"; + export const getReviews = async (req: Request, res: Response) => { const { review_receiver_id } = req.params; @@ -94,4 +96,38 @@ export const updateReview = async (req: Request, res: Response) => { logger.error(`Failed to update review for userID ${req.currentUser?.pi_uid}:`, error); return res.status(500).json({ message: 'An error occurred while updating review; please try again later' }); } -}; \ No newline at end of file +}; +export const applyTrustProtect = async (req: Request, res: Response) => { + try { + const authUser = req.currentUser; + const { review_id } = req.params; + + if (!authUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const updatedReview = await reviewFeedbackService.applyTrustProtect(review_id, authUser); + + // ✅ Send notification if Trust Protect changed rating to SAD + if (updatedReview && updatedReview.rating === 2) { + await notificationService.addNotification( + updatedReview.review_giver_id, + `Your review has been adjusted by Trust Protect you can reverse back the rating in the review screen.` + ); + logger.info( + `Notification sent to review giver ${updatedReview.review_giver_id} for Trust Protect adjustment.` + ); + } + + return res.status(200).json({ + message: "Trust Protect applied successfully", + updatedReview, + }); + } catch (error: any) { + logger.error( + `Failed to apply Trust Protect for review ${req.params.review_id}:`, + error + ); + return res.status(500).json({ message: "Failed to apply Trust Protect" }); + } +}; From 1244f76a2a267b2429e15bb606c1803bda46b776 Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 18 Oct 2025 12:12:02 -0400 Subject: [PATCH 3/6] Update membership routes: add endpoint for deductMappi operation --- src/routes/membership.routes.ts | 77 ++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/src/routes/membership.routes.ts b/src/routes/membership.routes.ts index 9d3430f..271ff41 100644 --- a/src/routes/membership.routes.ts +++ b/src/routes/membership.routes.ts @@ -24,12 +24,12 @@ import * as membershipController from "../controllers/membershipController"; * $numberDecimal: * type: string * required: - * - $numberDecimal + * - $numberDecimal * membership_expiry_date: * type: string * description: Membership expiration date * format: date-time - * + * * MembershipTiers: * type: object * properties: @@ -68,7 +68,10 @@ const membershipRoutes = Router(); * 500: * description: Internal server error */ -membershipRoutes.get("/membership-list", membershipController.getMembershipList); +membershipRoutes.get( + "/membership-list", + membershipController.getMembershipList +); /** * @swagger @@ -96,7 +99,10 @@ membershipRoutes.get("/membership-list", membershipController.getMembershipList) * 500: * description: Internal server error */ -membershipRoutes.get("/:membership_id", membershipController.getSingleMembership); +membershipRoutes.get( + "/:membership_id", + membershipController.getSingleMembership +); /** * @swagger @@ -121,7 +127,11 @@ membershipRoutes.get("/:membership_id", membershipController.getSingleMembership * 500: * description: Internal server error */ -membershipRoutes.get("/", verifyToken, membershipController.fetchUserMembership); +membershipRoutes.get( + "/", + verifyToken, + membershipController.fetchUserMembership +); /** * @swagger @@ -155,6 +165,59 @@ membershipRoutes.get("/", verifyToken, membershipController.fetchUserMembership) * 500: * description: Internal server error */ -membershipRoutes.put("/manage", verifyToken, membershipController.updateMembership); +membershipRoutes.put( + "/manage", + verifyToken, + membershipController.updateMembership +); -export default membershipRoutes; \ No newline at end of file +/** + * @swagger + * /api/v1/memberships/deduct-mappi: + * post: + * tags: + * - Membership + * summary: Deduct Mappi credits from a user for TrustProtect usage + * security: + * - bearerAuth: [] # Ensures only authenticated users can perform this action + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - amount + * properties: + * amount: + * type: number + * example: 100 + * description: Amount of Mappi to deduct (e.g., 100) + * responses: + * 200: + * description: Mappi deducted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * balance: + * type: number + * mappi_used_to_date: + * type: number + * 400: + * description: Invalid request body + * 401: + * description: Unauthorized access + * 500: + * description: Internal server error + */ +membershipRoutes.post( + "/deduct-mappi", + verifyToken, + membershipController.deductMappi +); + +export default membershipRoutes; From fc071f63b85d5bf384628ab24eb780fa2e0ff10a Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 18 Oct 2025 12:12:56 -0400 Subject: [PATCH 4/6] Update reviewFeedback routes: include Trust Protect endpoint --- src/routes/reviewFeedback.routes.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/routes/reviewFeedback.routes.ts b/src/routes/reviewFeedback.routes.ts index 5bf0082..6ed6331 100644 --- a/src/routes/reviewFeedback.routes.ts +++ b/src/routes/reviewFeedback.routes.ts @@ -191,5 +191,34 @@ reviewFeedbackRoutes.put( upload.single("image"), reviewFeedbackController.updateReview ); +/** + * @swagger + * /api/v1/review-feedback/trust-protect/{review_id}: + * put: + * tags: + * - Review Feedback + * summary: Apply Trust Protect on a review (admin/moderation action) + * parameters: + * - name: review_id + * in: path + * required: true + * schema: + * type: string + * description: The ID of the review to apply Trust Protect on + * responses: + * 200: + * description: Trust Protect applied successfully + * 401: + * description: Unauthorized + * 404: + * description: Review not found + * 500: + * description: Internal server error + */ +reviewFeedbackRoutes.put( + "/trust-protect/:review_id", + verifyToken, + reviewFeedbackController.applyTrustProtect +); export default reviewFeedbackRoutes; \ No newline at end of file From 5a6303ca5722b0b322d3ed97c89f3ab6fc33c368 Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 18 Oct 2025 12:14:21 -0400 Subject: [PATCH 5/6] Added Mappi deduction logic and integrate with user balance update in membership service --- src/services/membership.service.ts | 177 +++++++++++++++++++++-------- 1 file changed, 132 insertions(+), 45 deletions(-) diff --git a/src/services/membership.service.ts b/src/services/membership.service.ts index d7b524b..68de915 100644 --- a/src/services/membership.service.ts +++ b/src/services/membership.service.ts @@ -4,13 +4,16 @@ import { getTierByClass, getTierRank, } from "../helpers/membership"; +import { addNotification } from "./notification.service"; // For sending notifications +import { computeRatings } from "./reviewFeedback.service"; // Or wherever you defined it + import Membership from "../models/Membership"; import User from "../models/User"; -import { - MembershipClassType, - membershipTiers, - MappiCreditType, - mappiCreditOptions +import { + MembershipClassType, + membershipTiers, + MappiCreditType, + mappiCreditOptions, } from "../models/enums/membershipClassType"; import { IMembership, IUser, MembershipOption } from "../types"; @@ -53,7 +56,9 @@ const handleSingleMappiPurchase = async ( ).exec(); if (!updatedMembership) { - throw new Error(`Membership not found for update to base for piUID ${ user.pi_uid }`); + throw new Error( + `Membership not found for update to base for piUID ${user.pi_uid}` + ); } return updatedMembership; } else { @@ -65,12 +70,16 @@ const handleSingleMappiPurchase = async ( ).exec(); if (!updatedMembership) { - throw new Error(`Membership not found for update to increment for piUID ${ user.pi_uid }`); + throw new Error( + `Membership not found for update to increment for piUID ${user.pi_uid}` + ); } return updatedMembership; } } catch (error) { - logger.error(`Failed to handle single mappi purchase for ${user.pi_uid}: ${error}`); + logger.error( + `Failed to handle single mappi purchase for ${user.pi_uid}: ${error}` + ); throw error; } }; @@ -88,13 +97,17 @@ const handleMembershipTierPurchase = async ( const today = new Date(); const durationMs = (tier.DURATION ?? 0) * 7 * 24 * 60 * 60 * 1000; // weeks to ms - const newExpiryDate = tier.DURATION ? new Date(today.getTime() + durationMs) : null; + const newExpiryDate = tier.DURATION + ? new Date(today.getTime() + durationMs) + : null; const mappiAllowance = tier.MAPPI_ALLOWANCE; const newRank = tier.RANK; // If no existing membership, create new membership if (!existing) { - logger.info(`Creating new membership ${membership_class} for user ${user.pi_uid}`); + logger.info( + `Creating new membership ${membership_class} for user ${user.pi_uid}` + ); return await Membership.create({ user_id: user._id, pi_uid: user.pi_uid, @@ -107,40 +120,52 @@ const handleMembershipTierPurchase = async ( const currentRank = getTierRank(existing.membership_class); const expired = isExpired(existing.membership_expiry_date); - const sameClassType = isSameShoppingClassType(existing.membership_class, membership_class); + const sameClassType = isSameShoppingClassType( + existing.membership_class, + membership_class + ); let update: Partial = {}; // Different class type (e.g., online shopping vs offline shopping) if (!sameClassType) { - logger.info(`Switching membership type for ${user.pi_uid} to ${membership_class}`); + logger.info( + `Switching membership type for ${user.pi_uid} to ${membership_class}` + ); update = { membership_class, membership_expiry_date: newExpiryDate, mappi_balance: mappiAllowance, }; - // No rank change + // No rank change } else if (newRank === currentRank) { // Same rank & not expired then extend membership & add Mappi if (!expired) { - logger.info(`Extending membership ${membership_class} for ${user.pi_uid}`); + logger.info( + `Extending membership ${membership_class} for ${user.pi_uid}` + ); update = { membership_expiry_date: new Date( - (existing.membership_expiry_date?.getTime() ?? today.getTime()) + durationMs + (existing.membership_expiry_date?.getTime() ?? today.getTime()) + + durationMs ), $inc: { mappi_balance: mappiAllowance } as any, // atomic increment }; - // Same rank & expired then reset expiry & Mappi + // Same rank & expired then reset expiry & Mappi } else { - logger.info(`Renewing expired membership ${membership_class} for ${user.pi_uid}`); + logger.info( + `Renewing expired membership ${membership_class} for ${user.pi_uid}` + ); update = { membership_expiry_date: newExpiryDate, mappi_balance: mappiAllowance, }; } - // Rank change with upgrade or downgrade + // Rank change with upgrade or downgrade } else if (newRank !== currentRank) { - logger.info(`Changing rank from ${existing.membership_class} to ${membership_class} for ${user.pi_uid}`); + logger.info( + `Changing rank from ${existing.membership_class} to ${membership_class} for ${user.pi_uid}` + ); update = { membership_class, membership_expiry_date: newExpiryDate, @@ -158,10 +183,12 @@ const handleMembershipTierPurchase = async ( if (!updatedMembership) { throw new Error(`Membership not found during update for ${user.pi_uid}`); } - + return updatedMembership; } catch (error) { - logger.error(`Failed to handle membership tier purchase for ${user.pi_uid}: ${error}`); + logger.error( + `Failed to handle membership tier purchase for ${user.pi_uid}: ${error}` + ); throw error; } }; @@ -178,7 +205,7 @@ export const buildMembershipList = async (): Promise => { // Membership tiers except CASUAL const membershipOptions = Object.values(membershipTiers) - .filter(tier => tier.CLASS !== MembershipClassType.CASUAL) + .filter((tier) => tier.CLASS !== MembershipClassType.CASUAL) .map((tier) => ({ value: tier.CLASS as MembershipClassType, cost: tier.COST, @@ -186,47 +213,59 @@ export const buildMembershipList = async (): Promise => { mappi_allowance: tier.MAPPI_ALLOWANCE ?? 0, })) .sort((a, b) => { - const rankA = Object.values(membershipTiers).find(t => t.CLASS === a.value)?.RANK ?? 0; - const rankB = Object.values(membershipTiers).find(t => t.CLASS === b.value)?.RANK ?? 0; + const rankA = + Object.values(membershipTiers).find((t) => t.CLASS === a.value) + ?.RANK ?? 0; + const rankB = + Object.values(membershipTiers).find((t) => t.CLASS === b.value) + ?.RANK ?? 0; return rankA - rankB; }); - return [purchaseOptions, ...membershipOptions] + return [purchaseOptions, ...membershipOptions]; } catch (error) { - logger.error('Failed to build membership list', { error }); - throw error; + logger.error("Failed to build membership list", { error }); + throw error; } }; -export const getUserMembership = async (authUser: IUser): Promise => { +export const getUserMembership = async ( + authUser: IUser +): Promise => { try { - const membership = await Membership.findOne({ pi_uid: authUser.pi_uid }).lean(); - + const membership = await Membership.findOne({ + pi_uid: authUser.pi_uid, + }).lean(); + if (!membership) { const user = await User.findOne({ pi_uid: authUser.pi_uid }).lean(); const newMembership = await new Membership({ user_id: user?._id, pi_uid: authUser.pi_uid, membership_class: MembershipClassType.CASUAL, - membership_expiry_date: null, + membership_expiry_date: null, mappi_balance: 0, mappi_used_to_date: 0, - }).save() + }).save(); - return newMembership.toObject() + return newMembership.toObject(); } return membership; } catch (error) { - logger.error(`Failed to get user membership for piUID ${ authUser.pi_uid }: ${ error }`); + logger.error( + `Failed to get user membership for piUID ${authUser.pi_uid}: ${error}` + ); throw error; } }; export const getSingleMembershipById = async (membership_id: string) => { try { - return await Membership.findById(membership_id).lean() ?? null; + return (await Membership.findById(membership_id).lean()) ?? null; } catch (error) { - logger.error(`Failed to get single membership with ID ${ membership_id }: ${ error }`); + logger.error( + `Failed to get single membership with ID ${membership_id}: ${error}` + ); throw error; } }; @@ -240,18 +279,20 @@ export const updateMappiBalance = async (pi_uid: string, amount: number) => { ).exec(); if (!updatedMembership) { - throw new Error('Membership not found'); + throw new Error("Membership not found"); } return updatedMembership; } catch (error) { - logger.error(`Failed to update Mappi balance for piUID ${ pi_uid }: ${ error}`); + logger.error( + `Failed to update Mappi balance for piUID ${pi_uid}: ${error}` + ); throw error; } }; export const applyMembershipChange = async ( - piUid: string, + piUid: string, membership_class: MembershipClassType | MappiCreditType ): Promise => { try { @@ -264,18 +305,64 @@ export const applyMembershipChange = async ( // Get existing membership if any const existing = await Membership.findOne({ user_id: user?._id }); - // Single Mappi purchase case + // Single Mappi purchase case if (membership_class === MappiCreditType.SINGLE) { return await handleSingleMappiPurchase(existing, user); - } + } // Membership tier purchase case return await handleMembershipTierPurchase( - existing, user, + existing, + user, membership_class as MembershipClassType - ) + ); + } catch (error) { + logger.error(`Failed to apply membership change for ${piUid}: ${error}`); + throw error; + } +}; +/** + * Deduct Mappi for TrustProtect feature + * @param pi_uid - the user’s Pi UID + * @param amount - amount of Mappi to deduct (e.g., 100) + */ +export const deductMappi = async (pi_uid: string, amount: number) => { + try { + const membership = await Membership.findOne({ pi_uid }); + + if (!membership) { + logger.warn(`No membership found for user ${pi_uid}`); + throw new Error("Membership not found"); + } + + // Check balance first + if (membership.mappi_balance < amount) { + logger.warn( + `Insufficient Mappi balance for ${pi_uid}. Current: ${membership.mappi_balance}` + ); + throw new Error("Insufficient Mappi balance"); + } + + // Deduct Mappi and update usage stats + membership.mappi_balance -= amount; + membership.mappi_used_to_date += amount; + await membership.save(); + + logger.info( + `Successfully deducted ${amount} Mappi for TrustProtect for ${pi_uid}`, + { + balance: membership.mappi_balance, + mappi_used_to_date: membership.mappi_used_to_date, + } + ); + + return { + message: `Successfully deducted ${amount} Mappi.`, + balance: membership.mappi_balance, + mappi_used_to_date: membership.mappi_used_to_date, + }; } catch (error) { - logger.error(`Failed to apply membership change for ${ piUid }: ${ error }`); + logger.error(`Failed to deduct Mappi for ${pi_uid}:`, error); throw error; } -}; \ No newline at end of file +}; From 7ff302bc808f171dda87c522964a674a3ee011b9 Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 18 Oct 2025 12:16:15 -0400 Subject: [PATCH 6/6] Modified it to handle Trust Protect rating adjustment and trust meter recomputation --- src/services/reviewFeedback.service.ts | 203 +++++++++++++++++++------ 1 file changed, 154 insertions(+), 49 deletions(-) diff --git a/src/services/reviewFeedback.service.ts b/src/services/reviewFeedback.service.ts index 73b9a98..e979800 100644 --- a/src/services/reviewFeedback.service.ts +++ b/src/services/reviewFeedback.service.ts @@ -3,9 +3,15 @@ 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 logger from "../config/loggingConfig"; +import { RatingScale } from "../models/enums/ratingScale"; /** The value is set depending on the number of zero(0) ratings in the ReviewFeedback table where this user is review-receiver. @@ -16,18 +22,26 @@ import logger from "../config/loggingConfig"; When the User Registration screen is first used (before the users User record has been created) then the value of “100” is displayed and saved to the DB. **/ -const computeRatings = async (user_settings_id: string) => { +export 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(); // Calculate the percentage of zero ratings const zeroRatingsPercentage = (zeroRatingsCount / totalReviews) * 100; @@ -35,13 +49,13 @@ const computeRatings = async (user_settings_id: string) => { // 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: @@ -49,23 +63,28 @@ const computeRatings = async (user_settings_id: string) => { } // 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 => { 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; @@ -74,12 +93,16 @@ 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) => { @@ -87,11 +110,15 @@ 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, + }; }) ); @@ -101,25 +128,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> => { @@ -133,7 +167,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( @@ -143,11 +179,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, + }; }) ); @@ -157,42 +197,54 @@ 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 => { +export const addReviewFeedback = async ( + authUser: IUser, + formData: any, + image: string +): Promise => { try { const reviewFeedbackData: Partial = { - 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(), }; 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}`); return savedReviewFeedback as IReviewFeedback; } catch (error: any) { - logger.error(`Failed to add review: ${ error }`); + logger.error(`Failed to add review: ${error}`); throw error; } }; @@ -206,15 +258,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; } @@ -232,6 +288,55 @@ 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; -}; \ No newline at end of file +}; + +export const applyTrustProtect = async ( + reviewId: string, + authUser: IUser +): Promise => { + try { + // Find the review + const review = await ReviewFeedback.findById(reviewId).exec(); + if (!review) { + logger.error(`Review with ID ${reviewId} not found`); + const error = new Error(`Review with ID ${reviewId} not found`); + error.name = "NotFoundError"; + throw error; + } + + // Ensure user owns the review + if (review.review_receiver_id !== authUser.pi_uid) { + logger.error( + `User ${authUser.pi_uid} has no permission to Trust Protect this review` + ); + const error = new Error(`Forbidden`); + error.name = "ForbiddenError"; + throw error; + } + + // Update rating (e.g., from 1 = Despair → 2 = Sad) + if (review.rating === RatingScale.DESPAIR) { + review.rating = RatingScale.SAD; + await review.save(); + await computeRatings(review.review_receiver_id); + logger.info( + `Trust Protect applied to review ${reviewId} by user ${authUser.pi_uid}` + ); + return review as IReviewFeedback; + } else { + logger.warn( + `Trust Protect not applicable. Review ${reviewId} rating is not DESPAIR` + ); + return review; + } + } catch (error: any) { + logger.error( + `Failed to apply Trust Protect for reviewID ${reviewId}: ${error}` + ); + throw error; + } +};