All files / src/components/whatsapp template-utils.ts

90% Statements 45/50
76.66% Branches 23/30
100% Functions 15/15
97.56% Lines 40/41

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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164                                                217x 217x 217x 217x                         308x   308x 123x 123x 123x   123x 123x   29x   44x   15x   29x 44x             308x                                       1x 1x     1x 1x 1x 1x 1x     1x   1x 1x       1x     1x                       16x 22x 22x               304x                 109x 109x                 107x   64x     107x                 107x 107x    
// Utility functions for WhatsApp template parameter handling.
// Templates store their structure as a JSON string in the `components` column.
// These helpers parse that JSON, extract {{N}} variable slots, build the
// WhatsApp Cloud API `components` payload, and render preview text.
 
export type TemplateComponent = {
	type: string; // "HEADER" | "BODY" | "FOOTER" | "BUTTONS"
	format?: string; // "TEXT" | "IMAGE" | etc.
	text?: string;
	example?: { body_text?: string[][]; header_text?: string[] };
};
 
export type ParameterSlot = {
	componentType: "HEADER" | "BODY";
	index: number; // 1-based (matches {{1}}, {{2}}, etc.)
};
 
/**
 * Safely parse the JSON `components` string stored on a template.
 * Returns an empty array on null/invalid input.
 */
export function parseTemplateComponents(
	componentsJson: string | null,
): TemplateComponent[] {
	Iif (!componentsJson) return [];
	try {
		const parsed = JSON.parse(componentsJson);
		return Array.isArray(parsed) ? parsed : [];
	} catch {
		return [];
	}
}
 
/**
 * Scan parsed components for `{{N}}` placeholders and return a flat list
 * of slots grouped by component type (HEADER first, then BODY).
 */
export function extractParameterSlots(
	components: TemplateComponent[],
): ParameterSlot[] {
	const slots: ParameterSlot[] = [];
 
	for (const comp of components) {
		const type = comp.type?.toUpperCase();
		Iif (type !== "HEADER" && type !== "BODY") continue;
		Iif (!comp.text) continue;
 
		const matches = comp.text.match(/\{\{(\d+)\}\}/g);
		if (!matches) continue;
 
		const indices = [
			...new Set(
				matches.map((m) => Number.parseInt(m.replace(/\{|\}/g, ""), 10)),
			),
		].sort((a, b) => a - b);
 
		for (const idx of indices) {
			slots.push({
				componentType: type as "HEADER" | "BODY",
				index: idx,
			});
		}
	}
 
	return slots;
}
 
/**
 * Build the WhatsApp Cloud API `components` array from slot values.
 *
 * Example output:
 * ```json
 * [
 *   { "type": "body", "parameters": [
 *       { "type": "text", "text": "John" },
 *       { "type": "text", "text": "kitchen remodel" }
 *   ]}
 * ]
 * ```
 */
export function buildSendComponents(
	templateComponents: TemplateComponent[],
	paramValues: Record<string, string>,
): unknown[] {
	const slots = extractParameterSlots(templateComponents);
	Iif (slots.length === 0) return [];
 
	// Group slots by component type
	const grouped: Record<string, ParameterSlot[]> = {};
	for (const slot of slots) {
		const key = slot.componentType.toLowerCase();
		Eif (!grouped[key]) grouped[key] = [];
		grouped[key].push(slot);
	}
 
	const result: unknown[] = [];
 
	for (const [type, typeSlots] of Object.entries(grouped)) {
		const parameters = typeSlots.map((slot) => ({
			type: "text" as const,
			text: paramValues[`${slot.componentType}_${slot.index}`] || "",
		}));
		result.push({ type, parameters });
	}
 
	return result;
}
 
/**
 * Replace `{{N}}` placeholders in text with actual values for preview.
 * `values` is a map keyed like "BODY_1", "BODY_2", "HEADER_1", etc.
 */
export function renderPreviewText(
	text: string,
	componentType: "HEADER" | "BODY",
	values: Record<string, string>,
): string {
	return text.replace(/\{\{(\d+)\}\}/g, (match, num) => {
		const key = `${componentType}_${num}`;
		return values[key] || match;
	});
}
 
/**
 * Check whether a template has an approved status (case-insensitive).
 */
export function isApprovedTemplate(template: { status: string }): boolean {
	return template.status === "APPROVED" || template.status === "approved";
}
 
/**
 * Get the body text from parsed template components.
 */
export function getTemplateBodyText(
	components: TemplateComponent[],
): string | null {
	const body = components.find((c) => c.type?.toUpperCase() === "BODY");
	return body?.text ?? null;
}
 
/**
 * Get the header text from parsed template components (text headers only).
 */
export function getTemplateHeaderText(
	components: TemplateComponent[],
): string | null {
	const header = components.find(
		(c) =>
			c.type?.toUpperCase() === "HEADER" &&
			c.format?.toUpperCase() === "TEXT",
	);
	return header?.text ?? null;
}
 
/**
 * Get the footer text from parsed template components.
 */
export function getTemplateFooterText(
	components: TemplateComponent[],
): string | null {
	const footer = components.find((c) => c.type?.toUpperCase() === "FOOTER");
	return footer?.text ?? null;
}