import { Activity, type ActivityVerifications, type ActivityVideoUrls } from "@blade-and-brawn/domain"; import type { Static } from "@sinclair/typebox"; import { t } from "elysia"; import { db } from "../database/db"; export const VerificationStatusSchema = t.Union([ t.Literal("request"), t.Literal("in_review"), t.Literal("action_requested"), t.Literal("completed"), ]); export type VerificationStatus = Static; export class VerificationsService { async request(assessmentId: string, accountId: string, activityVideoUrls: ActivityVideoUrls) { // Only the owning account may request verification of their own assessment. const assessment = await db.selectFrom("assessments") .select("id") .where("id", "=", assessmentId) .where("account_id", "=", accountId) .executeTakeFirst(); if (!assessment) return undefined; return await db.insertInto("verifications") .values({ assessment_id: assessmentId, status: "request" satisfies VerificationStatus, url_back_squat: activityVideoUrls[Activity.BackSquat] ?? null, url_deadlift: activityVideoUrls[Activity.Deadlift] ?? null, url_bench_press: activityVideoUrls[Activity.BenchPress] ?? null, url_broad_jump: activityVideoUrls[Activity.BroadJump] ?? null, url_run: activityVideoUrls[Activity.Run] ?? null, url_cone_drill: activityVideoUrls[Activity.ConeDrill] ?? null, }) .onConflict((oc) => oc .column("assessment_id") .doUpdateSet((eb) => ({ status: "request" satisfies VerificationStatus, url_back_squat: eb.ref("excluded.url_back_squat"), url_deadlift: eb.ref("excluded.url_deadlift"), url_bench_press: eb.ref("excluded.url_bench_press"), url_broad_jump: eb.ref("excluded.url_broad_jump"), url_run: eb.ref("excluded.url_run"), url_cone_drill: eb.ref("excluded.url_cone_drill"), // Re-requesting means new videos are under review, so the // previous cycle's per-activity judgments and completion // info no longer apply. verf_back_squat: null, verf_deadlift: null, verf_bench_press: null, verf_broad_jump: null, verf_run: null, verf_cone_drill: null, completed_at: null, completed_by: null, })) ) .returning("id") .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 VerificationsService.baseQuery() .$if(opt.filter?.status !== undefined, (qb) => qb .where("verifications.status", "=", opt.filter!.status!) ) .orderBy("verifications.created_at", "desc") .$if(opt.limit !== undefined, (qb) => qb.limit(opt.limit!)) .$if(opt.offset !== undefined, (qb) => qb.offset(opt.offset!)) .execute(); } async requestAction(id: string, reviewerNotes: string | null, activityVerifications: ActivityVerifications) { const result = await db.updateTable("verifications") .where("id", "=", id) .set({ status: "action_requested" satisfies VerificationStatus, reviewer_notes: reviewerNotes, verf_back_squat: activityVerifications[Activity.BackSquat] ?? null, verf_deadlift: activityVerifications[Activity.Deadlift] ?? null, verf_bench_press: activityVerifications[Activity.BenchPress] ?? null, verf_broad_jump: activityVerifications[Activity.BroadJump] ?? null, verf_run: activityVerifications[Activity.Run] ?? null, verf_cone_drill: activityVerifications[Activity.ConeDrill] ?? null, }) .executeTakeFirst(); return result.numUpdatedRows > 0n; } async complete(id: string, completedBy: string, activityVerifications: ActivityVerifications) { const result = await db.updateTable("verifications") .where("id", "=", id) .set({ status: "completed" satisfies VerificationStatus, completed_at: new Date(), completed_by: completedBy, verf_back_squat: activityVerifications[Activity.BackSquat] ?? null, verf_deadlift: activityVerifications[Activity.Deadlift] ?? null, verf_bench_press: activityVerifications[Activity.BenchPress] ?? null, verf_broad_jump: activityVerifications[Activity.BroadJump] ?? null, verf_run: activityVerifications[Activity.Run] ?? null, verf_cone_drill: activityVerifications[Activity.ConeDrill] ?? null, }) .executeTakeFirst(); return result.numUpdatedRows > 0n; } }