import { prisma } from "../database"; import type { House, HouseMembership, HouseInvitation, HouseRequest, HouseRole, InvitationStatus, RequestStatus, Prisma, } from "@/prisma/generated/prisma/client"; import { ValidationError, NotFoundError, ConflictError, ForbiddenError, } from "../errors"; const HOUSE_NAME_MIN_LENGTH = 3; const HOUSE_NAME_MAX_LENGTH = 50; const HOUSE_DESCRIPTION_MAX_LENGTH = 500; export interface CreateHouseInput { name: string; description?: string | null; creatorId: string; } export interface UpdateHouseInput { name?: string; description?: string | null; } export interface InviteUserInput { houseId: string; inviterId: string; inviteeId: string; } export interface RequestToJoinInput { houseId: string; requesterId: string; } /** * Service de gestion des maisons */ export class HouseService { /** * Récupère une maison par son ID */ async getHouseById( id: string, include?: Prisma.HouseInclude ): Promise { return prisma.house.findUnique({ where: { id }, include, }); } /** * Récupère toutes les maisons avec pagination */ async getAllHouses(options?: { skip?: number; take?: number; include?: Prisma.HouseInclude; orderBy?: Prisma.HouseOrderByWithRelationInput; }): Promise { return prisma.house.findMany({ skip: options?.skip, take: options?.take, include: options?.include, orderBy: options?.orderBy || { createdAt: "desc" }, }); } /** * Recherche des maisons par nom */ async searchHouses( searchTerm: string, options?: { skip?: number; take?: number; include?: Prisma.HouseInclude; } ): Promise { return prisma.house.findMany({ where: { name: { contains: searchTerm, mode: "insensitive", }, }, skip: options?.skip, take: options?.take, include: options?.include, orderBy: { createdAt: "desc" }, }); } /** * Récupère les maisons d'un utilisateur */ async getUserHouses( userId: string, include?: Prisma.HouseInclude ): Promise { const memberships = await prisma.houseMembership.findMany({ where: { userId }, include: { house: { include: include, }, }, }); return memberships.map((m) => m.house); } /** * Récupère la maison d'un utilisateur (s'il en a une) */ async getUserHouse( userId: string, include?: Prisma.HouseInclude ): Promise { const membership = await prisma.houseMembership.findFirst({ where: { userId }, include: { house: { include: include, }, }, orderBy: { joinedAt: "asc" }, // Première maison jointe }); return membership?.house || null; } /** * Vérifie si un utilisateur est membre d'une maison */ async isUserMemberOfHouse( userId: string, houseId: string ): Promise { const membership = await prisma.houseMembership.findUnique({ where: { houseId_userId: { houseId, userId, }, }, }); return !!membership; } /** * Vérifie si un utilisateur est propriétaire ou admin d'une maison */ async isUserOwnerOrAdmin( userId: string, houseId: string ): Promise { const membership = await prisma.houseMembership.findUnique({ where: { houseId_userId: { houseId, userId, }, }, }); return membership?.role === "OWNER" || membership?.role === "ADMIN"; } /** * Vérifie si un utilisateur est propriétaire d'une maison */ async isUserOwner(userId: string, houseId: string): Promise { const membership = await prisma.houseMembership.findUnique({ where: { houseId_userId: { houseId, userId, }, }, }); return membership?.role === "OWNER"; } /** * Récupère le rôle d'un utilisateur dans une maison */ async getUserRole( userId: string, houseId: string ): Promise { const membership = await prisma.houseMembership.findUnique({ where: { houseId_userId: { houseId, userId, }, }, }); return membership?.role || null; } /** * Récupère les membres d'une maison */ async getHouseMembers( houseId: string, include?: Prisma.HouseMembershipInclude ): Promise { return prisma.houseMembership.findMany({ where: { houseId }, include, orderBy: [ { role: "asc" }, // OWNER, ADMIN, MEMBER { joinedAt: "asc" }, ], }); } /** * Crée une nouvelle maison */ async createHouse(data: CreateHouseInput): Promise { // Validation if (!data.name || data.name.trim().length === 0) { throw new ValidationError("Le nom de la maison est requis", "name"); } if ( data.name.length < HOUSE_NAME_MIN_LENGTH || data.name.length > HOUSE_NAME_MAX_LENGTH ) { throw new ValidationError( `Le nom de la maison doit contenir entre ${HOUSE_NAME_MIN_LENGTH} et ${HOUSE_NAME_MAX_LENGTH} caractères`, "name" ); } if (data.description && data.description.length > HOUSE_DESCRIPTION_MAX_LENGTH) { throw new ValidationError( `La description ne peut pas dépasser ${HOUSE_DESCRIPTION_MAX_LENGTH} caractères`, "description" ); } // Vérifier si l'utilisateur est déjà dans une maison const existingMembership = await prisma.houseMembership.findFirst({ where: { userId: data.creatorId }, }); if (existingMembership) { throw new ConflictError( "Vous êtes déjà membre d'une maison. Vous devez quitter votre maison actuelle avant d'en créer une nouvelle." ); } // Vérifier si le nom est déjà pris const existingHouse = await prisma.house.findFirst({ where: { name: { equals: data.name.trim(), mode: "insensitive", }, }, }); if (existingHouse) { throw new ConflictError("Ce nom de maison est déjà utilisé"); } // Créer la maison et ajouter le créateur comme OWNER return prisma.house.create({ data: { name: data.name.trim(), description: data.description?.trim() || null, creatorId: data.creatorId, memberships: { create: { userId: data.creatorId, role: "OWNER", }, }, }, }); } /** * Met à jour une maison */ async updateHouse( houseId: string, userId: string, data: UpdateHouseInput ): Promise { // Vérifier que l'utilisateur est propriétaire ou admin const isAuthorized = await this.isUserOwnerOrAdmin(userId, houseId); if (!isAuthorized) { throw new ForbiddenError( "Vous n'avez pas les permissions pour modifier cette maison" ); } const updateData: Prisma.HouseUpdateInput = {}; if (data.name !== undefined) { if (!data.name || data.name.trim().length === 0) { throw new ValidationError("Le nom de la maison est requis", "name"); } if ( data.name.length < HOUSE_NAME_MIN_LENGTH || data.name.length > HOUSE_NAME_MAX_LENGTH ) { throw new ValidationError( `Le nom de la maison doit contenir entre ${HOUSE_NAME_MIN_LENGTH} et ${HOUSE_NAME_MAX_LENGTH} caractères`, "name" ); } // Vérifier si le nom est déjà pris par une autre maison const existingHouse = await prisma.house.findFirst({ where: { name: { equals: data.name.trim(), mode: "insensitive", }, NOT: { id: houseId }, }, }); if (existingHouse) { throw new ConflictError("Ce nom de maison est déjà utilisé"); } updateData.name = data.name.trim(); } if (data.description !== undefined) { if (data.description && data.description.length > HOUSE_DESCRIPTION_MAX_LENGTH) { throw new ValidationError( `La description ne peut pas dépasser ${HOUSE_DESCRIPTION_MAX_LENGTH} caractères`, "description" ); } updateData.description = data.description?.trim() || null; } return prisma.house.update({ where: { id: houseId }, data: updateData, }); } /** * Supprime une maison */ async deleteHouse(houseId: string, userId: string): Promise { // Vérifier que l'utilisateur est propriétaire const isOwner = await this.isUserOwner(userId, houseId); if (!isOwner) { throw new ForbiddenError( "Seul le propriétaire peut supprimer la maison" ); } await prisma.house.delete({ where: { id: houseId }, }); } /** * Invite un utilisateur à rejoindre une maison */ async inviteUser(data: InviteUserInput): Promise { // Vérifier que l'inviteur est membre de la maison const isMember = await this.isUserMemberOfHouse( data.inviterId, data.houseId ); if (!isMember) { throw new ForbiddenError( "Vous devez être membre de la maison pour inviter quelqu'un" ); } // Vérifier que l'invité n'est pas déjà membre const isAlreadyMember = await this.isUserMemberOfHouse( data.inviteeId, data.houseId ); if (isAlreadyMember) { throw new ConflictError("Cet utilisateur est déjà membre de la maison"); } // Vérifier qu'il n'y a pas déjà une invitation en attente const existingInvitation = await prisma.houseInvitation.findUnique({ where: { houseId_inviteeId: { houseId: data.houseId, inviteeId: data.inviteeId, }, }, }); if (existingInvitation && existingInvitation.status === "PENDING") { throw new ConflictError( "Une invitation est déjà en attente pour cet utilisateur" ); } // Vérifier que l'invité n'est pas déjà dans une autre maison const existingMembership = await prisma.houseMembership.findFirst({ where: { userId: data.inviteeId }, }); if (existingMembership) { throw new ConflictError( "Cet utilisateur est déjà membre d'une autre maison" ); } // Créer l'invitation return prisma.houseInvitation.create({ data: { houseId: data.houseId, inviterId: data.inviterId, inviteeId: data.inviteeId, status: "PENDING", }, }); } /** * Accepte une invitation */ async acceptInvitation( invitationId: string, userId: string ): Promise { const invitation = await prisma.houseInvitation.findUnique({ where: { id: invitationId }, }); if (!invitation) { throw new NotFoundError("Invitation"); } if (invitation.inviteeId !== userId) { throw new ForbiddenError("Cette invitation ne vous est pas destinée"); } if (invitation.status !== "PENDING") { throw new ConflictError("Cette invitation n'est plus valide"); } // Vérifier que l'utilisateur n'est pas déjà dans une maison const existingMembership = await prisma.houseMembership.findFirst({ where: { userId }, }); if (existingMembership) { throw new ConflictError( "Vous êtes déjà membre d'une maison. Vous devez quitter votre maison actuelle avant d'accepter cette invitation." ); } // Créer le membership et mettre à jour l'invitation return prisma.$transaction(async (tx) => { const membership = await tx.houseMembership.create({ data: { houseId: invitation.houseId, userId: invitation.inviteeId, role: "MEMBER", }, }); await tx.houseInvitation.update({ where: { id: invitationId }, data: { status: "ACCEPTED" }, }); // Annuler toutes les autres invitations en attente pour cet utilisateur await tx.houseInvitation.updateMany({ where: { inviteeId: userId, status: "PENDING", id: { not: invitationId }, }, data: { status: "CANCELLED" }, }); // Annuler toutes les demandes en attente pour cet utilisateur await tx.houseRequest.updateMany({ where: { requesterId: userId, status: "PENDING", }, data: { status: "CANCELLED" }, }); return membership; }); } /** * Refuse une invitation */ async rejectInvitation(invitationId: string, userId: string): Promise { const invitation = await prisma.houseInvitation.findUnique({ where: { id: invitationId }, }); if (!invitation) { throw new NotFoundError("Invitation"); } if (invitation.inviteeId !== userId) { throw new ForbiddenError("Cette invitation ne vous est pas destinée"); } if (invitation.status !== "PENDING") { throw new ConflictError("Cette invitation n'est plus valide"); } await prisma.houseInvitation.update({ where: { id: invitationId }, data: { status: "REJECTED" }, }); } /** * Annule une invitation (par l'inviteur) */ async cancelInvitation(invitationId: string, userId: string): Promise { const invitation = await prisma.houseInvitation.findUnique({ where: { id: invitationId }, }); if (!invitation) { throw new NotFoundError("Invitation"); } if (invitation.inviterId !== userId) { throw new ForbiddenError( "Vous ne pouvez annuler que vos propres invitations" ); } if (invitation.status !== "PENDING") { throw new ConflictError("Cette invitation n'est plus valide"); } await prisma.houseInvitation.update({ where: { id: invitationId }, data: { status: "CANCELLED" }, }); } /** * Demande à rejoindre une maison */ async requestToJoin(data: RequestToJoinInput): Promise { // Vérifier que l'utilisateur n'est pas déjà membre const isMember = await this.isUserMemberOfHouse( data.requesterId, data.houseId ); if (isMember) { throw new ConflictError("Vous êtes déjà membre de cette maison"); } // Vérifier que l'utilisateur n'est pas déjà dans une autre maison const existingMembership = await prisma.houseMembership.findFirst({ where: { userId: data.requesterId }, }); if (existingMembership) { throw new ConflictError( "Vous êtes déjà membre d'une maison. Vous devez quitter votre maison actuelle avant de faire une demande." ); } // Vérifier qu'il n'y a pas déjà une demande en attente const existingRequest = await prisma.houseRequest.findUnique({ where: { houseId_requesterId: { houseId: data.houseId, requesterId: data.requesterId, }, }, }); if (existingRequest && existingRequest.status === "PENDING") { throw new ConflictError( "Une demande est déjà en attente pour cette maison" ); } // Créer la demande return prisma.houseRequest.create({ data: { houseId: data.houseId, requesterId: data.requesterId, status: "PENDING", }, }); } /** * Accepte une demande d'adhésion */ async acceptRequest( requestId: string, userId: string ): Promise { const request = await prisma.houseRequest.findUnique({ where: { id: requestId }, include: { house: true }, }); if (!request) { throw new NotFoundError("Demande"); } // Vérifier que l'utilisateur est propriétaire ou admin de la maison const isAuthorized = await this.isUserOwnerOrAdmin( userId, request.houseId ); if (!isAuthorized) { throw new ForbiddenError( "Vous n'avez pas les permissions pour accepter cette demande" ); } if (request.status !== "PENDING") { throw new ConflictError("Cette demande n'est plus valide"); } // Vérifier que le demandeur n'est pas déjà dans une maison const existingMembership = await prisma.houseMembership.findFirst({ where: { userId: request.requesterId }, }); if (existingMembership) { throw new ConflictError( "Cet utilisateur est déjà membre d'une autre maison" ); } // Créer le membership et mettre à jour la demande return prisma.$transaction(async (tx) => { const membership = await tx.houseMembership.create({ data: { houseId: request.houseId, userId: request.requesterId, role: "MEMBER", }, }); await tx.houseRequest.update({ where: { id: requestId }, data: { status: "ACCEPTED" }, }); // Annuler toutes les autres demandes en attente pour cet utilisateur await tx.houseRequest.updateMany({ where: { requesterId: request.requesterId, status: "PENDING", id: { not: requestId }, }, data: { status: "CANCELLED" }, }); // Annuler toutes les invitations en attente pour cet utilisateur await tx.houseInvitation.updateMany({ where: { inviteeId: request.requesterId, status: "PENDING", }, data: { status: "CANCELLED" }, }); return membership; }); } /** * Refuse une demande d'adhésion */ async rejectRequest(requestId: string, userId: string): Promise { const request = await prisma.houseRequest.findUnique({ where: { id: requestId }, }); if (!request) { throw new NotFoundError("Demande"); } // Vérifier que l'utilisateur est propriétaire ou admin de la maison const isAuthorized = await this.isUserOwnerOrAdmin( userId, request.houseId ); if (!isAuthorized) { throw new ForbiddenError( "Vous n'avez pas les permissions pour refuser cette demande" ); } if (request.status !== "PENDING") { throw new ConflictError("Cette demande n'est plus valide"); } await prisma.houseRequest.update({ where: { id: requestId }, data: { status: "REJECTED" }, }); } /** * Annule une demande (par le demandeur) */ async cancelRequest(requestId: string, userId: string): Promise { const request = await prisma.houseRequest.findUnique({ where: { id: requestId }, }); if (!request) { throw new NotFoundError("Demande"); } if (request.requesterId !== userId) { throw new ForbiddenError( "Vous ne pouvez annuler que vos propres demandes" ); } if (request.status !== "PENDING") { throw new ConflictError("Cette demande n'est plus valide"); } await prisma.houseRequest.update({ where: { id: requestId }, data: { status: "CANCELLED" }, }); } /** * Quitte une maison */ async leaveHouse(houseId: string, userId: string): Promise { const membership = await prisma.houseMembership.findUnique({ where: { houseId_userId: { houseId, userId, }, }, }); if (!membership) { throw new NotFoundError("Membre"); } // Le propriétaire ne peut pas quitter sa maison if (membership.role === "OWNER") { throw new ForbiddenError( "Le propriétaire ne peut pas quitter sa maison. Vous devez d'abord transférer la propriété ou supprimer la maison." ); } await prisma.houseMembership.delete({ where: { houseId_userId: { houseId, userId, }, }, }); } /** * Récupère les invitations reçues par un utilisateur */ async getUserInvitations( userId: string, status?: InvitationStatus ): Promise { return prisma.houseInvitation.findMany({ where: { inviteeId: userId, ...(status && { status }), }, include: { house: true, inviter: { select: { id: true, username: true, avatar: true, }, }, }, orderBy: { createdAt: "desc" }, }); } /** * Récupère les demandes d'une maison */ async getHouseRequests( houseId: string, status?: RequestStatus ): Promise { return prisma.houseRequest.findMany({ where: { houseId, ...(status && { status }), }, include: { requester: { select: { id: true, username: true, avatar: true, }, }, }, orderBy: { createdAt: "desc" }, }); } /** * Récupère les invitations envoyées par une maison */ async getHouseInvitations( houseId: string, status?: InvitationStatus ): Promise { return prisma.houseInvitation.findMany({ where: { houseId, ...(status && { status }), }, include: { invitee: { select: { id: true, username: true, avatar: true, }, }, inviter: { select: { id: true, username: true, avatar: true, }, }, }, orderBy: { createdAt: "desc" }, }); } /** * Récupère une invitation par son ID (avec seulement houseId) */ async getInvitationById( id: string ): Promise<{ houseId: string } | null> { return prisma.houseInvitation.findUnique({ where: { id }, select: { houseId: true }, }); } } export const houseService = new HouseService();