fix: revoke points when a comment is deleted
All checks were successful
Build and Push Backend Image / build (push) Successful in 43s

- revokePoints now accepts optional recordingId; when absent it deletes
  one matching row (for actions like COMMENT_CREATE that have no recording)
- deleteComment queues revokePoints + checkAchievements so leaderboard
  and social achievements stay in sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 11:16:18 +01:00
parent 04b0ec1a71
commit 79932157bf
3 changed files with 40 additions and 12 deletions

View File

@@ -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;
},
}),

View File

@@ -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<void> {
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);
}

View File

@@ -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 {