Add a verification interface for review

This commit is contained in:
Dominic Ferrando
2026-09-19 17:57:52 -04:00
parent f593aabd25
commit dc7ebd6873
6 changed files with 772 additions and 4 deletions
@@ -0,0 +1,95 @@
import { Activity, Gender, inToCm, lbToKg, minToMs, secToMs } from "@blade-and-brawn/domain";
import { db } from "../database/db";
import { env, log } from "../util";
import type { AccountRole } from "../services/accounts";
import { VerificationsService } from "../services/verifications";
const TEST_DISCORD_ID = "test-verification-seed";
const TEST_ASSESSMENT_NAME = "Test Verification Player";
async function seedTestAccount(): Promise<string> {
const existing = await db.selectFrom("accounts")
.select("id")
.where("discord_id", "=", TEST_DISCORD_ID)
.executeTakeFirst();
if (existing) {
log.info({ id: existing.id }, "test account already seeded, reusing");
return existing.id;
}
const account = await db.insertInto("accounts")
.values({
discord_id: TEST_DISCORD_ID,
name: "Test Player",
role: "user" satisfies AccountRole,
})
.returning("id")
.executeTakeFirstOrThrow();
log.info({ id: account.id }, "seeded test account");
return account.id;
}
async function seedTestAssessment(accountId: string): Promise<string> {
const existing = await db.selectFrom("assessments")
.select("id")
.where("account_id", "=", accountId)
.where("name", "=", TEST_ASSESSMENT_NAME)
.executeTakeFirst();
if (existing) {
log.info({ id: existing.id }, "test assessment already seeded, reusing");
return existing.id;
}
const result = await db.insertInto("assessments")
.values({
account_id: accountId,
name: TEST_ASSESSMENT_NAME,
gender: Gender.Male,
age: 22,
weight: lbToKg(180),
perf_back_squat: lbToKg(225),
perf_deadlift: lbToKg(315),
perf_bench_press: lbToKg(185),
perf_broad_jump: inToCm(96),
perf_run: minToMs(7) + secToMs(30),
perf_cone_drill: secToMs(7),
})
.returning("id")
.executeTakeFirstOrThrow();
log.info({ id: result.id }, "seeded test assessment");
return result.id;
}
(async () => {
const answer = prompt(`Seed a test verification request in the database (${env.DATABASE_URL})? (y/N)`);
if (answer?.trim().toLowerCase() !== "y") {
log.info("Aborted");
await db.destroy();
return;
}
try {
const accountId = await seedTestAccount();
const assessmentId = await seedTestAssessment(accountId);
const verifications = new VerificationsService();
const verification = await verifications.request(assessmentId, accountId, {
[Activity.BackSquat]: "https://example.com/videos/back-squat.mp4",
[Activity.Deadlift]: "https://example.com/videos/deadlift.mp4",
[Activity.BenchPress]: "https://example.com/videos/bench-press.mp4",
[Activity.Run]: "https://example.com/videos/run.mp4",
[Activity.BroadJump]: "https://example.com/videos/broad-jump.mp4",
[Activity.ConeDrill]: "https://example.com/videos/cone-drill.mp4",
});
if (!verification) throw new Error("Failed to seed verification request — assessment ownership check failed");
log.info({ id: verification.id, assessmentId, accountId }, "seeded test verification request");
}
catch (err) {
log.error({ err }, "seed failed");
process.exit(1);
}
finally {
await db.destroy();
}
})();
+8
View File
@@ -555,6 +555,13 @@ export const app = new Elysia()
offset: t.Optional(t.Numeric()),
})
})
.get("/:id", async ({ params: { id } }) => {
const verification = await s.Verifications.get(id);
if (!verification) throw new NotFoundError("Verification not found");
return verification;
}, {
params: t.Object({ id: t.String() })
})
.post("/:id/request-action", async ({ params: { id }, body: { reviewerNotes, activityVerifications } }) => {
const updated = await s.Verifications.requestAction(id, reviewerNotes ?? null, activityVerifications);
if (!updated) throw new NotFoundError("Verification not found");
@@ -684,3 +691,4 @@ app.listen(3000, async () => {
export type API = typeof app
export { type EventStatus } from "./services/events";
export { type VerificationStatus } from "./services/verifications";
+47 -4
View File
@@ -59,17 +59,60 @@ export class VerificationsService {
.executeTakeFirstOrThrow();
}
// Joined with the assessment being verified, since reviewing a request
// requires seeing the player's claimed performance alongside the videos.
private static baseQuery() {
return db.selectFrom("verifications")
.innerJoin("assessments", "assessments.id", "verifications.assessment_id")
.select([
"verifications.id",
"verifications.assessment_id",
"verifications.status",
"verifications.created_at",
"verifications.completed_at",
"verifications.completed_by",
"verifications.reviewer_notes",
"verifications.url_back_squat",
"verifications.url_deadlift",
"verifications.url_bench_press",
"verifications.url_broad_jump",
"verifications.url_run",
"verifications.url_cone_drill",
"verifications.verf_back_squat",
"verifications.verf_deadlift",
"verifications.verf_bench_press",
"verifications.verf_broad_jump",
"verifications.verf_run",
"verifications.verf_cone_drill",
"assessments.name",
"assessments.gender",
"assessments.age",
"assessments.weight",
"assessments.perf_back_squat",
"assessments.perf_deadlift",
"assessments.perf_bench_press",
"assessments.perf_broad_jump",
"assessments.perf_run",
"assessments.perf_cone_drill",
]);
}
async get(id: string) {
return await VerificationsService.baseQuery()
.where("verifications.id", "=", id)
.executeTakeFirst();
}
async list(opt: {
filter?: { status?: VerificationStatus },
limit?: number,
offset?: number,
} = {}) {
return await db.selectFrom("verifications")
.selectAll()
return await VerificationsService.baseQuery()
.$if(opt.filter?.status !== undefined, (qb) => qb
.where("status", "=", opt.filter!.status!)
.where("verifications.status", "=", opt.filter!.status!)
)
.orderBy("created_at", "desc")
.orderBy("verifications.created_at", "desc")
.$if(opt.limit !== undefined, (qb) => qb.limit(opt.limit!))
.$if(opt.offset !== undefined, (qb) => qb.offset(opt.offset!))
.execute();