All files / routes/admin/whatsapp templates.routes.ts

100% Statements 48/48
100% Branches 13/13
100% Functions 5/5
100% Lines 47/47

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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143                                  2x     2x 3x 3x 3x 2x   1x         2x 11x 11x 11x             11x             5x               6x 6x 1x               5x   5x       4x   1x                                   2x 3x 3x 3x 3x   3x 3x   3x 1x     2x 1x   1x         2x 2x 2x 2x   2x   2x 1x   1x         2x 2x 2x   2x   2x 1x   1x          
import { Hono } from "hono";
import type { Dal } from "../../../dal";
import { error, handleError, success } from "../../../lib/response";
import { requireWhatsAppClient } from "../../../lib/whatsapp/client";
import type { WhatsAppTemplateCreateRequest } from "../../../lib/whatsapp/types";
import type { Services } from "../../../services";
 
type Env = {
	Bindings: CloudflareBindings;
	Variables: {
		user: { id: string; name: string; email: string } | null;
		session: unknown;
		dal: Dal;
		services: Services;
	};
};
 
const templates = new Hono<Env>();
 
// GET / - List all templates
templates.get("/", async (c) => {
	try {
		const services = c.get("services");
		const items = await services.waTemplate.list();
		return success(c, { templates: items });
	} catch (err) {
		return handleError(c, err);
	}
});
 
// POST / - Create template
templates.post("/", async (c) => {
	try {
		const services = c.get("services");
		const body = await c.req.json<{
			name?: string;
			language?: string;
			category?: string;
			components?: unknown[];
		}>();
 
		if (
			!body.name ||
			!body.language ||
			!body.category ||
			!Array.isArray(body.components) ||
			body.components.length === 0
		) {
			return error(
				c,
				"VALIDATION_ERROR",
				"name, language, category, and components (non-empty array) are required",
				400,
			);
		}
 
		const validCategories = ["MARKETING", "UTILITY", "AUTHENTICATION"];
		if (!validCategories.includes(body.category)) {
			return error(
				c,
				"VALIDATION_ERROR",
				`category must be one of: ${validCategories.join(", ")}`,
				400,
			);
		}
 
		const client = requireWhatsAppClient(c.env);
 
		const template = await services.waTemplate.create(
			body as WhatsAppTemplateCreateRequest,
			client,
		);
		return success(c, { template }, 201);
	} catch (err) {
		return handleError(c, err);
	}
});
 
// PUT /:id - Edit template (only components can be updated after creation)
//
// KEPT DELIBERATELY, AND API-ONLY. The portal's `whatsappApi.updateTemplate`
// client method had no caller — no hook, no edit affordance on the templates
// page — and is being removed. The route is not, because it is the only way to
// amend a submitted template's components, and a template already sent to Meta
// is otherwise immutable from our side.
//
// Verified before keeping it: template SYNC does not go through here. `POST
// /sync` calls `services.waTemplate.sync()`, which upserts via
// `dal.waTemplates.upsertByName` and never touches `services.waTemplate
// .update()`. The Meta status webhook likewise writes through the DAL
// directly. So this handler has exactly one entry point — a direct API call —
// and deleting it would remove a capability rather than dead weight.
templates.put("/:id", async (c) => {
	try {
		const services = c.get("services");
		const id = Number(c.req.param("id"));
		const body = await c.req.json<{ components?: unknown[] }>();
 
		const allowed: Record<string, unknown> = {};
		if (body.components !== undefined) allowed.components = body.components;
 
		if (Object.keys(allowed).length === 0) {
			return error(c, "VALIDATION_ERROR", "No updatable fields provided", 400);
		}
 
		const template = await services.waTemplate.update(id, allowed);
		return success(c, { template });
	} catch (err) {
		return handleError(c, err);
	}
});
 
// DELETE /:id - Delete template
templates.delete("/:id", async (c) => {
	try {
		const services = c.get("services");
		const id = Number(c.req.param("id"));
 
		const client = requireWhatsAppClient(c.env);
 
		await services.waTemplate.delete(id, client);
		return success(c, { message: "Template deleted" });
	} catch (err) {
		return handleError(c, err);
	}
});
 
// POST /sync - Force sync from Meta API
templates.post("/sync", async (c) => {
	try {
		const services = c.get("services");
 
		const client = requireWhatsAppClient(c.env);
 
		const synced = await services.waTemplate.sync(client);
		return success(c, { synced });
	} catch (err) {
		return handleError(c, err);
	}
});
 
export default templates;