All files / services/whatsapp template.service.ts

100% Statements 31/31
100% Branches 16/16
100% Functions 8/8
100% Lines 27/27

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                    25x     2x       3x 3x 1x               4x 4x   4x 4x                 4x     4x                   2x   2x                                 3x 3x   2x 2x 1x             2x 2x   1x 1x                 6x                 6x 6x         6x 5x              
import type { Dal } from "../../dal";
import type { WaTemplate, NewWaTemplate } from "../../db/schema";
import type {
	WhatsAppClient,
	WhatsAppTemplateCreateRequest,
	WhatsAppTemplateStatusWebhook,
} from "../../lib/whatsapp";
import { NotFoundError } from "../../lib/errors";
 
export class TemplateService {
	constructor(private dal: Dal) {}
 
	async list(): Promise<WaTemplate[]> {
		return this.dal.waTemplates.findAll();
	}
 
	async getById(id: number): Promise<WaTemplate> {
		const template = await this.dal.waTemplates.findById(id);
		if (!template) throw new NotFoundError("Template", String(id));
		return template;
	}
 
	/**
	 * Sync templates from Meta API to local DB.
	 * Upserts by name+language.
	 */
	async sync(whatsAppClient: WhatsAppClient): Promise<number> {
		const response = await whatsAppClient.listTemplates();
		let synced = 0;
 
		for (const metaTemplate of response.data) {
			await this.dal.waTemplates.upsertByName({
				metaTemplateId: metaTemplate.id,
				name: metaTemplate.name,
				language: metaTemplate.language,
				category: metaTemplate.category as NewWaTemplate["category"],
				status: metaTemplate.status as NewWaTemplate["status"],
				components: JSON.stringify(metaTemplate.components),
				rejectionReason: metaTemplate.rejected_reason ?? null,
			});
			synced++;
		}
 
		return synced;
	}
 
	/**
	 * Create template on Meta + store locally.
	 */
	async create(
		data: WhatsAppTemplateCreateRequest,
		whatsAppClient: WhatsAppClient,
	): Promise<WaTemplate> {
		const result = await whatsAppClient.createTemplate(data);
 
		return this.dal.waTemplates.create({
			metaTemplateId: result.id,
			name: data.name,
			language: data.language,
			category: data.category as NewWaTemplate["category"],
			status: "PENDING",
			components: JSON.stringify(data.components),
		});
	}
 
	/**
	 * Update template locally (Meta only allows editing rejected templates).
	 */
	async update(
		id: number,
		data: Partial<Pick<WaTemplate, "components" | "rejectionReason">>,
	): Promise<WaTemplate> {
		const template = await this.dal.waTemplates.findById(id);
		if (!template) throw new NotFoundError("Template", String(id));
 
		const updated = await this.dal.waTemplates.update(id, data);
		if (!updated) throw new NotFoundError("Template", String(id));
		return updated;
	}
 
	/**
	 * Delete from Meta + local DB.
	 */
	async delete(id: number, whatsAppClient: WhatsAppClient): Promise<void> {
		const template = await this.dal.waTemplates.findById(id);
		if (!template) throw new NotFoundError("Template", String(id));
 
		await whatsAppClient.deleteTemplate(template.name);
		await this.dal.waTemplates.delete(id);
	}
 
	/**
	 * Handle template status webhook from Meta.
	 */
	async handleStatusWebhook(
		event: WhatsAppTemplateStatusWebhook,
	): Promise<void> {
		const statusMap: Record<string, NewWaTemplate["status"]> = {
			APPROVED: "APPROVED",
			REJECTED: "REJECTED",
			PENDING_DELETION: "DISABLED",
			DISABLED: "DISABLED",
			PAUSED: "PAUSED",
			IN_APPEAL: "PENDING",
		};
 
		const newStatus = statusMap[event.event] ?? "PENDING";
		const template = await this.dal.waTemplates.findByName(
			event.message_template_name,
			event.message_template_language,
		);
 
		if (template) {
			await this.dal.waTemplates.update(template.id, {
				status: newStatus,
				rejectionReason: event.reason ?? null,
			});
		}
	}
}