Implement basic verifications API endpoints
This commit is contained in:
@@ -7,7 +7,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
|||||||
await db.schema.createTable("verifications")
|
await db.schema.createTable("verifications")
|
||||||
.$call(addDefaultColumns)
|
.$call(addDefaultColumns)
|
||||||
.addColumn("assessment_id", "bigint", (cb) => cb.notNull().unique())
|
.addColumn("assessment_id", "bigint", (cb) => cb.notNull().unique())
|
||||||
.addColumn("status", "text", (cb) => cb.notNull().defaultTo("request")) // request | in_review | action_required | completed
|
.addColumn("status", "text", (cb) => cb.notNull().defaultTo("request")) // request | in_review | action_requested | completed
|
||||||
.addColumn("completed_at", "timestamptz")
|
.addColumn("completed_at", "timestamptz")
|
||||||
.addColumn("completed_by", "uuid")
|
.addColumn("completed_by", "uuid")
|
||||||
.addColumn("url_back_squat", "text")
|
.addColumn("url_back_squat", "text")
|
||||||
|
|||||||
+55
-1
@@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ActivityPerformanceSchema,
|
ActivityPerformanceSchema,
|
||||||
|
ActivityVerificationsSchema,
|
||||||
|
ActivityVideosSchema,
|
||||||
PlayerSchema,
|
PlayerSchema,
|
||||||
} from "@blade-and-brawn/domain"
|
} from "@blade-and-brawn/domain"
|
||||||
import { cors } from "@elysiajs/cors";
|
import { cors } from "@elysiajs/cors";
|
||||||
@@ -23,6 +25,7 @@ import { EventsService, EventStatusSchema } from "./services/events";
|
|||||||
import { AccountsService, type AccountRole } from "./services/accounts";
|
import { AccountsService, type AccountRole } from "./services/accounts";
|
||||||
import { Value } from "@sinclair/typebox/value";
|
import { Value } from "@sinclair/typebox/value";
|
||||||
import { AssessmentsService } from "./services/assessments";
|
import { AssessmentsService } from "./services/assessments";
|
||||||
|
import { VerificationsService, VerificationStatusSchema } from "./services/verifications";
|
||||||
import { Not } from "@sinclair/typebox";
|
import { Not } from "@sinclair/typebox";
|
||||||
|
|
||||||
// CONSTANTS
|
// CONSTANTS
|
||||||
@@ -40,7 +43,8 @@ const s = (() => {
|
|||||||
const Accounts = new AccountsService(Calculator);
|
const Accounts = new AccountsService(Calculator);
|
||||||
const Events = new EventsService();
|
const Events = new EventsService();
|
||||||
const Assessments = new AssessmentsService();
|
const Assessments = new AssessmentsService();
|
||||||
return { Standards, Calculator, Commerce, Accounts, Events, Assessments };
|
const Verifications = new VerificationsService();
|
||||||
|
return { Standards, Calculator, Commerce, Accounts, Events, Assessments, Verifications };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// QUEUES
|
// QUEUES
|
||||||
@@ -523,6 +527,56 @@ export const app = new Elysia()
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// VERIFICATIONS
|
||||||
|
.group("/verifications", (app) => app
|
||||||
|
.guard({ auth: true }, (app) => app
|
||||||
|
.post("/me", async ({ body: { assessmentId, activityVideoUrls }, accountId }) => {
|
||||||
|
const requested = await s.Verifications.request(assessmentId, accountId, activityVideoUrls);
|
||||||
|
if (!requested) throw new NotFoundError("Assessment not found");
|
||||||
|
return requested;
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
assessmentId: t.String(),
|
||||||
|
activityVideoUrls: ActivityVideosSchema,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.guard({ authAdmin: true }, (app) => app
|
||||||
|
.get("/", async ({ query }) => {
|
||||||
|
return await s.Verifications.list({
|
||||||
|
filter: { status: query.status },
|
||||||
|
limit: query.limit,
|
||||||
|
offset: query.offset,
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
query: t.Object({
|
||||||
|
status: t.Optional(VerificationStatusSchema),
|
||||||
|
limit: t.Optional(t.Numeric()),
|
||||||
|
offset: t.Optional(t.Numeric()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.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");
|
||||||
|
}, {
|
||||||
|
params: t.Object({ id: t.String() }),
|
||||||
|
body: t.Object({
|
||||||
|
reviewerNotes: t.Optional(t.String()),
|
||||||
|
activityVerifications: ActivityVerificationsSchema,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post("/:id/complete", async ({ params: { id }, body: { activityVerifications }, accountId }) => {
|
||||||
|
const updated = await s.Verifications.complete(id, accountId, activityVerifications);
|
||||||
|
if (!updated) throw new NotFoundError("Verification not found");
|
||||||
|
}, {
|
||||||
|
params: t.Object({ id: t.String() }),
|
||||||
|
body: t.Object({
|
||||||
|
activityVerifications: ActivityVerificationsSchema,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// WEBHOOKS
|
// WEBHOOKS
|
||||||
.post("/webhooks/printful", async ({ body, query }) => {
|
.post("/webhooks/printful", async ({ body, query }) => {
|
||||||
// https://webflow.com/integrations/printful
|
// https://webflow.com/integrations/printful
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
import { Activity, type ActivityVideoUrls } from "@blade-and-brawn/domain";
|
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";
|
import { db } from "../database/db";
|
||||||
|
|
||||||
export type VerificationStatus = "request" | "in_review" | "action_required" | "completed";
|
export const VerificationStatusSchema = t.Union([
|
||||||
|
t.Literal("request"),
|
||||||
|
t.Literal("in_review"),
|
||||||
|
t.Literal("action_requested"),
|
||||||
|
t.Literal("completed"),
|
||||||
|
]);
|
||||||
|
export type VerificationStatus = Static<typeof VerificationStatusSchema>;
|
||||||
|
|
||||||
export class VerificationsService {
|
export class VerificationsService {
|
||||||
async request(assessmentId: string, activityVideoUrls: ActivityVideoUrls) {
|
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")
|
return await db.insertInto("verifications")
|
||||||
.values({
|
.values({
|
||||||
assessment_id: assessmentId,
|
assessment_id: assessmentId,
|
||||||
@@ -58,4 +74,39 @@ export class VerificationsService {
|
|||||||
.$if(opt.offset !== undefined, (qb) => qb.offset(opt.offset!))
|
.$if(opt.offset !== undefined, (qb) => qb.offset(opt.offset!))
|
||||||
.execute();
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,3 +45,6 @@ export type ActivityPerformance = Static<typeof ActivityPerformanceSchema>;
|
|||||||
|
|
||||||
export const ActivityVideosSchema = t.Partial(t.Record(t.Enum(Activity), t.String()));
|
export const ActivityVideosSchema = t.Partial(t.Record(t.Enum(Activity), t.String()));
|
||||||
export type ActivityVideoUrls = Static<typeof ActivityVideosSchema>;
|
export type ActivityVideoUrls = Static<typeof ActivityVideosSchema>;
|
||||||
|
|
||||||
|
export const ActivityVerificationsSchema = t.Partial(t.Record(t.Enum(Activity), t.Boolean()));
|
||||||
|
export type ActivityVerifications = Static<typeof ActivityVerificationsSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user