diff --git a/backend/typescript/middlewares/validators/petValidators.ts b/backend/typescript/middlewares/validators/petValidators.ts index 67a0865e..126aab1b 100644 --- a/backend/typescript/middlewares/validators/petValidators.ts +++ b/backend/typescript/middlewares/validators/petValidators.ts @@ -171,3 +171,27 @@ export const petRequestDtoValidators = async ( // return next(); // }; + +export const matchPetsValidator = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const { userId } = req.params; + + if (!userId) { + return res.status(400).json({ error: "userId parameter is required." }); + } + + if (typeof userId !== "string") { + return res + .status(400) + .json({ error: "userId parameter must be a string." }); + } + + if (Number.isNaN(Number(userId))) { + return res.status(400).json({ error: "Invalid user ID" }); + } + + return next(); +}; diff --git a/backend/typescript/middlewares/validators/userValidators.ts b/backend/typescript/middlewares/validators/userValidators.ts index f0e1ad73..4e2afd74 100644 --- a/backend/typescript/middlewares/validators/userValidators.ts +++ b/backend/typescript/middlewares/validators/userValidators.ts @@ -146,3 +146,25 @@ export const updateUserDtoValidator = async ( } return next(); }; + +export const matchUsersValidator = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const { petId } = req.params; + + if (!petId) { + return res.status(400).json({ error: "petId parameter is required." }); + } + + if (typeof petId !== "string") { + return res.status(400).json({ error: "petId parameter must be a string." }); + } + + if (Number.isNaN(Number(petId))) { + return res.status(400).json({ error: "Invalid pet ID" }); + } + + return next(); +}; diff --git a/backend/typescript/rest/petRoutes.ts b/backend/typescript/rest/petRoutes.ts index 8c205a8e..c2bad710 100644 --- a/backend/typescript/rest/petRoutes.ts +++ b/backend/typescript/rest/petRoutes.ts @@ -1,6 +1,7 @@ import { Router } from "express"; import { petRequestDtoValidators, + matchPetsValidator, /* // petFilterValidators, */ } from "../middlewares/validators/petValidators"; import PetService from "../services/implementations/petService"; @@ -11,9 +12,13 @@ import { NotFoundError, } from "../utilities/errorUtils"; import { sendResponseByMimeType } from "../utilities/responseUtil"; +import IUserService from "../services/interfaces/userService"; +import UserService from "../services/implementations/userService"; +import { UserDTO } from "../types"; const petRouter: Router = Router(); const petService: IPetService = new PetService(); +const userService: IUserService = new UserService(); /* Update Pet by id */ petRouter.put("/:id", petRequestDtoValidators, async (req, res) => { @@ -140,4 +145,31 @@ petRouter.get("/:id", async (req, res) => { } }); +petRouter.get("/match/pets/:userId", matchPetsValidator, async (req, res) => { + const { userId } = req.params; + const contentType = req.headers["content-type"]; + + try { + const user: UserDTO = await userService.getUserById(userId); + const matchingPets = await petService.getMatchingPetsForUser( + user.colorLevel, + ); + sendResponseByMimeType(res, 200, contentType, matchingPets); + } catch (error: unknown) { + if (error instanceof NotFoundError) { + sendResponseByMimeType(res, 400, contentType, [ + { + error: getErrorMessage(error), + }, + ]); + } else { + sendResponseByMimeType(res, 500, contentType, [ + { + error: getErrorMessage(error), + }, + ]); + } + } +}); + export default petRouter; diff --git a/backend/typescript/rest/userRoutes.ts b/backend/typescript/rest/userRoutes.ts index 69393010..6de6f277 100644 --- a/backend/typescript/rest/userRoutes.ts +++ b/backend/typescript/rest/userRoutes.ts @@ -4,6 +4,7 @@ import { getAccessToken } from "../middlewares/auth"; import { createUserDtoValidator, updateUserDtoValidator, + matchUsersValidator, } from "../middlewares/validators/userValidators"; import nodemailerConfig from "../nodemailer.config"; import AuthService from "../services/implementations/authService"; @@ -19,12 +20,15 @@ import { INTERNAL_SERVER_ERROR_MESSAGE, } from "../utilities/errorUtils"; import { sendResponseByMimeType } from "../utilities/responseUtil"; +import { IPetService, PetResponseDTO } from "../services/interfaces/petService"; +import PetService from "../services/implementations/petService"; const userRouter: Router = Router(); const userService: IUserService = new UserService(); const emailService: IEmailService = new EmailService(nodemailerConfig); const authService: IAuthService = new AuthService(userService, emailService); +const petService: IPetService = new PetService(); /* Get all users, optionally filter by a userId or email query parameter to retrieve a single user */ userRouter.get("/", async (req, res) => { @@ -325,4 +329,31 @@ userRouter.delete("/", async (req, res) => { .json({ error: "Must supply one of userId or email as query parameter." }); }); +userRouter.get("/match/users/:petId", matchUsersValidator, async (req, res) => { + const { petId } = req.params; + const contentType = req.headers["content-type"]; + + try { + const pet: PetResponseDTO = await petService.getPet(petId); + const matchingUsers = await userService.getMatchingUsersForPet( + pet.colorLevel, + ); + sendResponseByMimeType(res, 200, contentType, matchingUsers); + } catch (error: unknown) { + if (error instanceof NotFoundError) { + sendResponseByMimeType(res, 400, contentType, [ + { + error: getErrorMessage(error), + }, + ]); + } else { + sendResponseByMimeType(res, 500, contentType, [ + { + error: getErrorMessage(error), + }, + ]); + } + } +}); + export default userRouter; diff --git a/backend/typescript/services/implementations/petService.ts b/backend/typescript/services/implementations/petService.ts index 6eaa96dc..46165104 100644 --- a/backend/typescript/services/implementations/petService.ts +++ b/backend/typescript/services/implementations/petService.ts @@ -1,4 +1,4 @@ -import { Transaction } from "sequelize"; +import { Transaction, Op } from "sequelize"; import PgPet from "../../models/pet.model"; // import PgTask from "../../models/task.model"; // import PgTaskTemplate from "../../models/taskTemplate.model"; @@ -14,13 +14,13 @@ import { import { getErrorMessage, NotFoundError } from "../../utilities/errorUtils"; import logger from "../../utilities/logger"; import { sequelize } from "../../models"; -// import TaskTemplate from "../../models/taskTemplate.model"; -// import { Role } from "../../types"; +// import ActivityType from "../../models/activityType.model"; const Logger = logger(__filename); class PetService implements IPetService { /* eslint-disable class-methods-use-this */ + getAgeFromBirthday(birthday: string): number { const parsedBirthday = Date.parse(birthday); const currentDate = new Date(); @@ -314,6 +314,46 @@ class PetService implements IPetService { } return id; } + + async getMatchingPetsForUser( + userColorLevel: number, + ): Promise { + try { + const matchingPets = await PgPet.findAll({ + where: { + color_level: { + [Op.lte]: userColorLevel, + }, + }, + }); + + return matchingPets.map((pet) => ({ + id: pet.id, + name: pet.name, + animalTag: pet.animal_tag, + colorLevel: pet.color_level, + status: pet.status, + breed: pet.breed, + age: pet.birthday ? this.getAgeFromBirthday(pet.birthday) : undefined, + weight: pet.weight, + sex: pet.sex, + photo: pet.photo, + careInfo: { + id: pet.petCareInfo?.id, + safetyInfo: pet.petCareInfo?.safety_info, + medicalInfo: pet.petCareInfo?.medical_info, + managementInfo: pet.petCareInfo?.management_info, + }, + })); + } catch (error) { + Logger.error( + `Failed to get matching pets for user. Reason = ${getErrorMessage( + error, + )}`, + ); + throw error; + } + } } export default PetService; diff --git a/backend/typescript/services/implementations/userService.ts b/backend/typescript/services/implementations/userService.ts index 68fdf0de..5bfd9124 100644 --- a/backend/typescript/services/implementations/userService.ts +++ b/backend/typescript/services/implementations/userService.ts @@ -441,6 +441,23 @@ class UserService implements IUserService { throw error; } } + + async getMatchingUsersForPet(petColorLevel: number): Promise { + try { + const allUsers = await this.getUsers(); + + return allUsers.filter( + (user: UserDTO) => user.colorLevel >= petColorLevel, + ); + } catch (error) { + Logger.error( + `Failed to get matching users for pet. Reason = ${getErrorMessage( + error, + )}`, + ); + throw error; + } + } } export default UserService; diff --git a/backend/typescript/services/interfaces/matchmakingService.ts b/backend/typescript/services/interfaces/matchmakingService.ts new file mode 100644 index 00000000..4aaf0af5 --- /dev/null +++ b/backend/typescript/services/interfaces/matchmakingService.ts @@ -0,0 +1,8 @@ +import { PetResponseDTO } from "./petService"; +import { UserDTO } from "../../types"; + +export interface IMatchmakingService { + /* eslint-disable class-methods-use-this */ + getMatchingPetsForUser(userId: string): Promise; + getMatchingUsersForPet(petId: string): Promise; +} diff --git a/backend/typescript/services/interfaces/petService.ts b/backend/typescript/services/interfaces/petService.ts index 8389a516..2cb4ad84 100644 --- a/backend/typescript/services/interfaces/petService.ts +++ b/backend/typescript/services/interfaces/petService.ts @@ -97,6 +97,14 @@ export interface IPetService { */ deletePet(id: string): Promise; + /** + * get all pets the user can take care of + * @param userColorLevel user's color level + * @returns list of pets + * @throws Error if matching fails + */ + getMatchingPetsForUser(userColorLevel: number): Promise; + /** * retrieve all Pets that match given filter criteria * @param query Pet queries diff --git a/backend/typescript/services/interfaces/userService.ts b/backend/typescript/services/interfaces/userService.ts index 795e6913..f28b053d 100644 --- a/backend/typescript/services/interfaces/userService.ts +++ b/backend/typescript/services/interfaces/userService.ts @@ -79,6 +79,14 @@ interface IUserService { * @throws Error if user deletion fails */ deleteUserByEmail(email: string): Promise; + + /** + * Get all users that a pet can take care of + * @param petColorLevel pet's color level + * @returns list of users + * @throws Error if failed to get matching pets + */ + getMatchingUsersForPet(petColorLevel: number): Promise; } export default IUserService;