All files / routes/admin consultations.routes.ts

100% Statements 28/28
100% Branches 16/16
100% Functions 2/2
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113                                                      1x             1x     1x 5x 5x 5x 5x 5x   5x       1x                       4x   4x           3x           1x         1x 6x 6x 6x 6x 1x     5x 5x 2x               3x       2x 1x     1x   1x          
// Admin Routes — Free Consultation Bookings (ops dashboard)
//
// Read/update surface for /book-consultation submissions. Bookings are
// pro-less (see db/schema/consultation-bookings.ts) so there is no existing
// pro-scoped inquiry UI to reuse — ops previously worked these off the
// consultation_lead_internal email alone.
 
import { Hono } from "hono";
import type { Dal } from "../../dal";
import type { ConsultationBookingStatus } from "../../dal/consultation-bookings.dal";
import { buildPaginationMeta, getPagination } from "../../lib/pagination";
import {
	error,
	handleError,
	success,
	successWithPagination,
} from "../../lib/response";
 
type Env = {
	Bindings: CloudflareBindings;
	Variables: {
		user: { id: string; name: string; email: string } | null;
		session: unknown;
		dal: Dal;
	};
};
 
const VALID_STATUSES: ConsultationBookingStatus[] = [
	"pending",
	"contacted",
	"completed",
	"cancelled",
];
 
const consultations = new Hono<Env>();
 
// List bookings, newest first, optionally filtered by status.
consultations.get("/", async (c) => {
	try {
		const dal = c.get("dal");
		const page = Number(c.req.query("page") || "1");
		const limit = Number(c.req.query("limit") || "20");
		const statusParam = c.req.query("status");
 
		if (
			statusParam &&
			!VALID_STATUSES.includes(statusParam as ConsultationBookingStatus)
		) {
			return error(
				c,
				"VALIDATION_ERROR",
				`status must be one of: ${VALID_STATUSES.join(", ")}`,
				400,
			);
		}
 
		const {
			offset,
			limit: safeLimit,
			page: safePage,
		} = getPagination(page, limit);
 
		const { items, total } = await dal.consultationBookings.list({
			limit: safeLimit,
			offset,
			status: statusParam as ConsultationBookingStatus | undefined,
		});
 
		return successWithPagination(
			c,
			items,
			buildPaginationMeta(total, safePage, safeLimit),
		);
	} catch (err) {
		return handleError(c, err);
	}
});
 
// Update a booking's ops status (e.g. pending -> contacted -> completed).
consultations.patch("/:id", async (c) => {
	try {
		const dal = c.get("dal");
		const id = Number(c.req.param("id"));
		if (!Number.isInteger(id)) {
			return error(c, "VALIDATION_ERROR", "id must be a number", 400);
		}
 
		const body = await c.req.json();
		if (!body.status || !VALID_STATUSES.includes(body.status)) {
			return error(
				c,
				"VALIDATION_ERROR",
				`status is required and must be one of: ${VALID_STATUSES.join(", ")}`,
				400,
			);
		}
 
		const updated = await dal.consultationBookings.updateStatus(
			id,
			body.status,
		);
		if (!updated) {
			return error(c, "NOT_FOUND", "Consultation booking not found", 404);
		}
 
		return success(c, updated);
	} catch (err) {
		return handleError(c, err);
	}
});
 
export default consultations;