import { Activity, type ActivityVerifications, type ActivityMediaKeys } from "@blade-and-brawn/domain"; import type { Static } from "@sinclair/typebox"; import { t } from "elysia"; import type { Selectable } from "kysely"; import { db } from "../database/db"; import type { DB } from "../database/out/db"; export const VerificationStatusSchema = t.Union([ t.Literal("draft"), t.Literal("submitted"), t.Literal("in_review"), t.Literal("action_requested"), t.Literal("completed"), ]); export type VerificationStatus = Static; const MEDIA_KEY_COLUMN = { [Activity.BackSquat]: "media_key_back_squat", [Activity.Deadlift]: "media_key_deadlift", [Activity.BenchPress]: "media_key_bench_press", [Activity.BroadJump]: "media_key_broad_jump", [Activity.Run]: "media_key_run", [Activity.ConeDrill]: "media_key_cone_drill", } as const satisfies Record; const ALLOWED_FROM = { update: ["draft", "action_requested"], submit: ["draft"], requestAction: ["submitted", "in_review"], complete: ["submitted", "in_review"], } satisfies Record; export class VerificationsService { static readonly REQUIRED_MEDIA_ACTIVITIES = Object.values(Activity); static readonly ALLOWED_MEDIA_RE = /^(video\/(mp4|quicktime|webm)|image\/(png|jpeg))$/; static missingActivityMediaKeys(verification: Pick, (typeof MEDIA_KEY_COLUMN)[Activity]>) { return VerificationsService.REQUIRED_MEDIA_ACTIVITIES.filter((activity) => !verification[MEDIA_KEY_COLUMN[activity]]); } async create(assessmentId: string, activityMediaKeys?: ActivityMediaKeys) { return await db.insertInto("verifications") .values({ assessment_id: assessmentId, status: "draft" satisfies VerificationStatus, media_key_back_squat: activityMediaKeys?.[Activity.BackSquat] ?? null, media_key_deadlift: activityMediaKeys?.[Activity.Deadlift] ?? null, media_key_bench_press: activityMediaKeys?.[Activity.BenchPress] ?? null, media_key_broad_jump: activityMediaKeys?.[Activity.BroadJump] ?? null, media_key_run: activityMediaKeys?.[Activity.Run] ?? null, media_key_cone_drill: activityMediaKeys?.[Activity.ConeDrill] ?? null, }) .onConflict((oc) => oc.column("assessment_id").doNothing()) .returning(["id", "status"]) .executeTakeFirst(); } async update(id: string, { assessmentId, activityMediaKeys = {} }: { assessmentId?: string, activityMediaKeys?: ActivityMediaKeys }) { const replaced = (activity: Activity) => activityMediaKeys[activity] ? null : undefined; return await db.updateTable("verifications") .where("id", "=", id) .where("status", "in", ALLOWED_FROM.update) .set({ status: "draft" satisfies VerificationStatus, submitted_at: null, assessment_id: assessmentId, media_key_back_squat: activityMediaKeys[Activity.BackSquat], media_key_deadlift: activityMediaKeys[Activity.Deadlift], media_key_bench_press: activityMediaKeys[Activity.BenchPress], media_key_broad_jump: activityMediaKeys[Activity.BroadJump], media_key_run: activityMediaKeys[Activity.Run], media_key_cone_drill: activityMediaKeys[Activity.ConeDrill], verf_back_squat: replaced(Activity.BackSquat), verf_deadlift: replaced(Activity.Deadlift), verf_bench_press: replaced(Activity.BenchPress), verf_broad_jump: replaced(Activity.BroadJump), verf_run: replaced(Activity.Run), verf_cone_drill: replaced(Activity.ConeDrill), }) .returning(["id", "status"]) .executeTakeFirst(); } async get(id: string, accountId?: string) { return await VerificationsService.baseQuery() .where("verifications.id", "=", id) .$if(accountId !== undefined, (qb) => qb.where("assessments.account_id", "=", accountId!)) .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.submitted_at", (ob) => ob.desc().nullsLast()) .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 submit(id: string) { return await db.updateTable("verifications") .where("id", "=", id) .where("status", "in", ALLOWED_FROM.submit) .where((eb) => eb.and( VerificationsService.REQUIRED_MEDIA_ACTIVITIES.map((activity) => eb(MEDIA_KEY_COLUMN[activity], "is not", null)) )) .set({ status: "submitted" satisfies VerificationStatus, submitted_at: new Date() }) .returning(["id", "status"]) .executeTakeFirst(); } async requestAction(id: string, reviewerNotes: string | null, activityVerifications: ActivityVerifications) { return await db.updateTable("verifications") .where("id", "=", id) .where("status", "in", ALLOWED_FROM.requestAction) .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, }) .returning(["id", "status"]) .executeTakeFirst(); } async complete(id: string, completedBy: string, activityVerifications: ActivityVerifications) { return await db.updateTable("verifications") .where("id", "=", id) .where("status", "in", ALLOWED_FROM.complete) .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, }) .returning(["id", "status"]) .executeTakeFirst(); } 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.submitted_at", "verifications.completed_at", "verifications.completed_by", "verifications.reviewer_notes", "verifications.media_key_back_squat", "verifications.media_key_deadlift", "verifications.media_key_bench_press", "verifications.media_key_broad_jump", "verifications.media_key_run", "verifications.media_key_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", ]); } }