diff --git a/packages/backend/src/graphql/resolvers/comments.ts b/packages/backend/src/graphql/resolvers/comments.ts index 1a96d80..b670ee5 100644 --- a/packages/backend/src/graphql/resolvers/comments.ts +++ b/packages/backend/src/graphql/resolvers/comments.ts @@ -98,6 +98,18 @@ builder.mutationField("deleteComment", (t) => if (!comment[0]) throw new GraphQLError("Comment not found"); requireOwnerOrAdmin(ctx, comment[0].user_id); await ctx.db.delete(comments).where(eq(comments.id, args.id)); + + await gamificationQueue.add("revokePoints", { + job: "revokePoints", + userId: comment[0].user_id, + action: "COMMENT_CREATE", + }); + await gamificationQueue.add("checkAchievements", { + job: "checkAchievements", + userId: comment[0].user_id, + category: "social", + }); + return true; }, }), diff --git a/packages/backend/src/lib/gamification.ts b/packages/backend/src/lib/gamification.ts index 2e754de..91546e5 100644 --- a/packages/backend/src/lib/gamification.ts +++ b/packages/backend/src/lib/gamification.ts @@ -1,4 +1,4 @@ -import { eq, sql, and, gt, isNotNull, count, sum } from "drizzle-orm"; +import { eq, sql, and, gt, isNull, isNotNull, count, sum } from "drizzle-orm"; import type { DB } from "../db/connection"; import { user_points, @@ -45,17 +45,33 @@ export async function revokePoints( db: DB, userId: string, action: keyof typeof POINT_VALUES, - recordingId: string, + recordingId?: string, ): Promise { - await db - .delete(user_points) - .where( - and( - eq(user_points.user_id, userId), - eq(user_points.action, action), - eq(user_points.recording_id, recordingId), - ), - ); + const recordingCondition = recordingId + ? eq(user_points.recording_id, recordingId) + : isNull(user_points.recording_id); + + // When no recordingId (e.g. COMMENT_CREATE), delete only one row so each + // revoke undoes exactly one prior award. + if (!recordingId) { + const row = await db + .select({ id: user_points.id }) + .from(user_points) + .where( + and(eq(user_points.user_id, userId), eq(user_points.action, action), recordingCondition), + ) + .limit(1); + if (row[0]) { + await db.delete(user_points).where(eq(user_points.id, row[0].id)); + } + } else { + await db + .delete(user_points) + .where( + and(eq(user_points.user_id, userId), eq(user_points.action, action), recordingCondition), + ); + } + await updateUserStats(db, userId); } diff --git a/packages/backend/src/queues/workers/gamification.ts b/packages/backend/src/queues/workers/gamification.ts index 1bd1889..1245b16 100644 --- a/packages/backend/src/queues/workers/gamification.ts +++ b/packages/backend/src/queues/workers/gamification.ts @@ -9,7 +9,7 @@ const log = logger.child({ component: "gamification-worker" }); export type GamificationJobData = | { job: "awardPoints"; userId: string; action: keyof typeof POINT_VALUES; recordingId?: string } - | { job: "revokePoints"; userId: string; action: keyof typeof POINT_VALUES; recordingId: string } + | { job: "revokePoints"; userId: string; action: keyof typeof POINT_VALUES; recordingId?: string } | { job: "checkAchievements"; userId: string; category?: string }; export function startGamificationWorker(): Worker {