Files
got-gaming/services/challenges/challenge.service.ts

646 lines
15 KiB
TypeScript

import { prisma } from "../database";
import type {
Challenge,
ChallengeStatus,
Prisma,
} from "@/prisma/generated/prisma/client";
import { ValidationError, NotFoundError } from "../errors";
export interface CreateChallengeInput {
challengerId: string;
challengedId: string;
title: string;
description: string;
pointsReward?: number;
}
export interface UpdateChallengeInput {
status?: ChallengeStatus;
adminId?: string;
adminComment?: string;
winnerId?: string;
title?: string;
description?: string;
pointsReward?: number;
}
export interface ChallengeWithUsers extends Challenge {
challenger: {
id: string;
username: string;
avatar: string | null;
};
challenged: {
id: string;
username: string;
avatar: string | null;
};
admin?: {
id: string;
username: string;
} | null;
winner?: {
id: string;
username: string;
} | null;
}
/**
* Service de gestion des défis entre joueurs
*/
export class ChallengeService {
/**
* Crée un nouveau défi
*/
async createChallenge(data: CreateChallengeInput): Promise<Challenge> {
// Vérifier que les deux joueurs existent
const [challenger, challenged] = await Promise.all([
prisma.user.findUnique({ where: { id: data.challengerId } }),
prisma.user.findUnique({ where: { id: data.challengedId } }),
]);
if (!challenger) {
throw new NotFoundError("Joueur qui lance le défi");
}
if (!challenged) {
throw new NotFoundError("Joueur qui reçoit le défi");
}
// Vérifier qu'on ne se défie pas soi-même
if (data.challengerId === data.challengedId) {
throw new ValidationError("Vous ne pouvez pas vous défier vous-même");
}
return prisma.challenge.create({
data: {
challengerId: data.challengerId,
challengedId: data.challengedId,
title: data.title,
description: data.description,
pointsReward: data.pointsReward || 100,
status: "PENDING",
},
});
}
/**
* Accepte un défi
*/
async acceptChallenge(
challengeId: string,
userId: string
): Promise<Challenge> {
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que l'utilisateur est bien celui qui reçoit le défi
if (challenge.challengedId !== userId) {
throw new ValidationError("Vous n'êtes pas autorisé à accepter ce défi");
}
// Vérifier que le défi est en attente
if (challenge.status !== "PENDING") {
throw new ValidationError(
"Ce défi ne peut plus être accepté (statut: " + challenge.status + ")"
);
}
return prisma.challenge.update({
where: { id: challengeId },
data: {
status: "ACCEPTED",
acceptedAt: new Date(),
},
});
}
/**
* Accepte un défi en tant qu'admin (bypass les vérifications utilisateur)
*/
async adminAcceptChallenge(challengeId: string): Promise<Challenge> {
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que le défi est en attente
if (challenge.status !== "PENDING") {
throw new ValidationError(
"Ce défi ne peut plus être accepté (statut: " + challenge.status + ")"
);
}
return prisma.challenge.update({
where: { id: challengeId },
data: {
status: "ACCEPTED",
acceptedAt: new Date(),
},
});
}
/**
* Annule un défi (par le challenger ou le challenged)
*/
async cancelChallenge(
challengeId: string,
userId: string
): Promise<Challenge> {
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que l'utilisateur est bien impliqué dans le défi
if (
challenge.challengerId !== userId &&
challenge.challengedId !== userId
) {
throw new ValidationError("Vous n'êtes pas autorisé à annuler ce défi");
}
// Vérifier que le défi peut être annulé
if (challenge.status === "COMPLETED") {
throw new ValidationError("Un défi complété ne peut pas être annulé");
}
return prisma.challenge.update({
where: { id: challengeId },
data: {
status: "CANCELLED",
},
});
}
/**
* Valide un défi (admin seulement)
*/
async validateChallenge(
challengeId: string,
adminId: string,
winnerId: string,
adminComment?: string
): Promise<Challenge> {
// Récupérer uniquement les champs nécessaires pour la validation
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
select: {
id: true,
status: true,
challengerId: true,
challengedId: true,
pointsReward: true,
},
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que le défi est accepté
if (challenge.status !== "ACCEPTED") {
throw new ValidationError(
"Seuls les défis acceptés peuvent être validés"
);
}
// Vérifier que le winner est bien l'un des deux joueurs
if (
winnerId !== challenge.challengerId &&
winnerId !== challenge.challengedId
) {
throw new ValidationError(
"Le gagnant doit être l'un des deux joueurs du défi"
);
}
// Paralléliser la mise à jour du défi et l'attribution des points
const [updatedChallenge] = await Promise.all([
prisma.challenge.update({
where: { id: challengeId },
data: {
status: "COMPLETED",
adminId,
adminComment: adminComment || null,
winnerId,
completedAt: new Date(),
},
include: {
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
},
challenged: {
select: {
id: true,
username: true,
avatar: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
}),
prisma.user.update({
where: { id: winnerId },
data: {
score: {
increment: challenge.pointsReward,
},
},
}),
]);
return updatedChallenge;
}
/**
* Rejette un défi (admin seulement)
*/
async rejectChallenge(
challengeId: string,
adminId: string,
adminComment?: string
): Promise<Challenge> {
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que le défi est accepté
if (challenge.status !== "ACCEPTED") {
throw new ValidationError(
"Seuls les défis acceptés peuvent être rejetés"
);
}
return prisma.challenge.update({
where: { id: challengeId },
data: {
status: "REJECTED",
adminId,
adminComment: adminComment || null,
},
});
}
/**
* Met à jour un défi (admin seulement)
*/
async updateChallenge(
challengeId: string,
data: UpdateChallengeInput
): Promise<Challenge> {
const updateData: Prisma.ChallengeUpdateInput = {};
if (data.title !== undefined) {
updateData.title = data.title;
}
if (data.description !== undefined) {
updateData.description = data.description;
}
if (data.pointsReward !== undefined) {
updateData.pointsReward = data.pointsReward;
}
if (data.status !== undefined) {
updateData.status = data.status;
}
if (data.adminId !== undefined) {
updateData.admin = data.adminId
? { connect: { id: data.adminId } }
: { disconnect: true };
}
if (data.adminComment !== undefined) {
updateData.adminComment = data.adminComment;
}
if (data.winnerId !== undefined) {
updateData.winner = data.winnerId
? { connect: { id: data.winnerId } }
: { disconnect: true };
}
try {
return await prisma.challenge.update({
where: { id: challengeId },
data: updateData,
});
} catch (error: unknown) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "P2025"
) {
// Record not found
throw new NotFoundError("Défi");
}
throw error;
}
}
/**
* Annule un défi (admin seulement)
*/
async adminCancelChallenge(challengeId: string): Promise<Challenge> {
// Récupérer uniquement le statut pour la validation
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
select: {
id: true,
status: true,
},
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que le défi peut être annulé
if (challenge.status === "COMPLETED") {
throw new ValidationError("Un défi complété ne peut pas être annulé");
}
return prisma.challenge.update({
where: { id: challengeId },
data: {
status: "CANCELLED",
},
});
}
/**
* Réactive un défi annulé (admin seulement)
* Remet le défi en PENDING s'il n'avait jamais été accepté, sinon en ACCEPTED
*/
async reactivateChallenge(challengeId: string): Promise<Challenge> {
// Récupérer uniquement les champs nécessaires
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
select: {
id: true,
status: true,
acceptedAt: true,
},
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que le défi est annulé
if (challenge.status !== "CANCELLED") {
throw new ValidationError(
"Seuls les défis annulés peuvent être réactivés"
);
}
// Si le défi avait été accepté avant, le remettre en ACCEPTED
// Sinon, le remettre en PENDING
const newStatus = challenge.acceptedAt ? "ACCEPTED" : "PENDING";
return prisma.challenge.update({
where: { id: challengeId },
data: {
status: newStatus,
},
});
}
/**
* Supprime un défi (admin seulement)
*/
async deleteChallenge(challengeId: string): Promise<void> {
try {
await prisma.challenge.delete({
where: { id: challengeId },
});
} catch (error: unknown) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "P2025"
) {
// Record not found
throw new NotFoundError("Défi");
}
throw error;
}
}
/**
* Récupère un défi par son ID avec les utilisateurs
*/
async getChallengeById(id: string): Promise<ChallengeWithUsers | null> {
const challenge = await prisma.challenge.findUnique({
where: { id },
include: {
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
},
challenged: {
select: {
id: true,
username: true,
avatar: true,
},
},
admin: {
select: {
id: true,
username: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
});
return challenge as ChallengeWithUsers | null;
}
/**
* Récupère tous les défis d'un utilisateur
*/
async getUserChallenges(userId: string): Promise<ChallengeWithUsers[]> {
return prisma.challenge.findMany({
where: {
OR: [{ challengerId: userId }, { challengedId: userId }],
},
include: {
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
},
challenged: {
select: {
id: true,
username: true,
avatar: true,
},
},
admin: {
select: {
id: true,
username: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
orderBy: {
createdAt: "desc",
},
}) as Promise<ChallengeWithUsers[]>;
}
/**
* Récupère les défis acceptés en attente de désignation du gagnant
*/
async getPendingValidationChallenges(): Promise<ChallengeWithUsers[]> {
return prisma.challenge.findMany({
where: {
status: "ACCEPTED",
},
include: {
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
},
challenged: {
select: {
id: true,
username: true,
avatar: true,
},
},
admin: {
select: {
id: true,
username: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
orderBy: {
acceptedAt: "asc",
},
}) as Promise<ChallengeWithUsers[]>;
}
/**
* Récupère tous les défis (pour admin)
*/
async getAllChallenges(options?: {
status?: ChallengeStatus;
take?: number;
}): Promise<ChallengeWithUsers[]> {
return prisma.challenge.findMany({
where: options?.status ? { status: options.status } : undefined,
include: {
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
},
challenged: {
select: {
id: true,
username: true,
avatar: true,
},
},
admin: {
select: {
id: true,
username: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
orderBy: {
createdAt: "desc",
},
take: options?.take,
}) as Promise<ChallengeWithUsers[]>;
}
/**
* Compte les défis actifs pour un utilisateur
* - PENDING où l'utilisateur est challenged (en attente de sa réponse)
* - ACCEPTED où l'utilisateur est challenger ou challenged (en cours, en attente de validation)
*/
async getActiveChallengesCount(userId: string): Promise<number> {
return prisma.challenge.count({
where: {
OR: [
// Défis en attente de réponse de l'utilisateur
{
status: "PENDING",
challengedId: userId,
},
// Défis acceptés en cours (en attente de désignation du gagnant)
{
status: "ACCEPTED",
OR: [{ challengerId: userId }, { challengedId: userId }],
},
],
},
});
}
}
export const challengeService = new ChallengeService();