EventBuzz_Backend/src/services/whatsappTemplateMetaSync.js

144 lines
4.8 KiB
JavaScript

const WhatsAppTemplate = require('../models/WhatsAppTemplate');
const { getOrgWhatsAppCredentials, listMessageTemplates } = require('../utils/metaWhatsApp');
function normLang(s) {
return String(s || '')
.trim()
.replace(/-/g, '_')
.toLowerCase();
}
function rejectionFromMeta(m) {
if (!m || typeof m !== 'object') return '';
return String(m.rejected_reason || m.reason || '').trim();
}
function normalizeCategory(raw) {
const c = String(raw || '')
.trim()
.toUpperCase();
if (c === 'MARKETING' || c === 'UTILITY' || c === 'AUTHENTICATION') return c;
return 'UTILITY';
}
async function resolveCreatedBy(organizationId, locals) {
for (const loc of locals || []) {
if (loc.createdBy) return loc.createdBy;
}
try {
const User = require('../models/User');
const u = await User.findOne({ organization: organizationId }).select('_id').lean();
if (u?._id) return u._id;
} catch {
/* ignore */
}
return undefined;
}
/**
* Pull template rows from Meta and upsert local docs (by metaTemplateId or name+language).
* Creates local rows for Meta templates that are not yet in the dashboard.
* @param {import('mongoose').Types.ObjectId|string} organizationId
* @param {{ onlyNames?: string[] }} [opts] — when set, only upsert these Meta template names
*/
async function syncOrganizationTemplates(organizationId, opts = {}) {
const creds = await getOrgWhatsAppCredentials(organizationId);
if (!creds) {
return { ok: false, error: 'WhatsApp Cloud is not configured for this organisation (WABA + access token).' };
}
const data = await listMessageTemplates(creds.wabaId, creds.accessToken);
let list = Array.isArray(data.data) ? data.data : [];
const onlyNames = Array.isArray(opts.onlyNames)
? opts.onlyNames.map((n) => String(n || '').trim().toLowerCase()).filter(Boolean)
: null;
if (onlyNames && onlyNames.length) {
list = list.filter((m) => onlyNames.includes(String(m.name || '').trim().toLowerCase()));
}
const locals = await WhatsAppTemplate.find({ organization: organizationId }).lean();
const now = new Date();
let updated = 0;
let created = 0;
for (const loc of locals) {
let m = null;
if (loc.metaTemplateId) {
m = list.find((x) => String(x.id) === String(loc.metaTemplateId));
}
if (!m) {
m = list.find((x) => x.name === loc.name && normLang(x.language) === normLang(loc.language));
}
if (m) {
const st = m.status != null ? String(m.status).toUpperCase() : loc.status;
const $set = {
metaTemplateId: String(m.id),
status: st,
rejectionReason: rejectionFromMeta(m),
lastSyncedAt: now,
lastError: '',
};
if (m.category) $set.category = normalizeCategory(m.category);
if (Array.isArray(m.components) && m.components.length) $set.components = m.components;
await WhatsAppTemplate.updateOne({ _id: loc._id }, { $set });
updated += 1;
}
}
const createdBy = await resolveCreatedBy(organizationId, locals);
for (const m of list) {
const name = String(m.name || '').trim();
const language = String(m.language || '').trim().replace(/-/g, '_');
if (!name || !language) continue;
const exists = locals.some(
(loc) =>
(loc.metaTemplateId && String(loc.metaTemplateId) === String(m.id)) ||
(loc.name === name && normLang(loc.language) === normLang(language)),
);
if (exists) continue;
try {
await WhatsAppTemplate.create({
organization: organizationId,
name,
language,
category: normalizeCategory(m.category),
components: Array.isArray(m.components) ? m.components : [],
metaTemplateId: m.id != null ? String(m.id) : '',
status: m.status != null ? String(m.status).toUpperCase() : 'PENDING',
rejectionReason: rejectionFromMeta(m),
lastSyncedAt: now,
lastError: '',
variableBindings: { header: [], body: [], url: [] },
...(createdBy ? { createdBy } : {}),
});
created += 1;
} catch (e) {
if (e && e.code === 11000) {
await WhatsAppTemplate.updateOne(
{ organization: organizationId, name, language },
{
$set: {
metaTemplateId: m.id != null ? String(m.id) : '',
status: m.status != null ? String(m.status).toUpperCase() : 'PENDING',
rejectionReason: rejectionFromMeta(m),
lastSyncedAt: now,
lastError: '',
...(m.category ? { category: normalizeCategory(m.category) } : {}),
...(Array.isArray(m.components) && m.components.length ? { components: m.components } : {}),
},
},
);
updated += 1;
continue;
}
throw e;
}
}
return { ok: true, metaCount: list.length, updated, created };
}
module.exports = { syncOrganizationTemplates };