Merge pull request 'FEAT: syncFromMeta importa templates pre-aprovados da Meta' (#6) from fix/templates into dev
All checks were successful
Deploy Dev — Backend / Deploy para VM Dev (push) Successful in 7s
All checks were successful
Deploy Dev — Backend / Deploy para VM Dev (push) Successful in 7s
Reviewed-on: #6
This commit is contained in:
commit
abfcbf2aee
@ -163,6 +163,52 @@ export class WhatsappTemplateRepository {
|
|||||||
return result.rowCount || 0;
|
return result.rowCount || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async importFromMeta(input: {
|
||||||
|
metaTemplateId: string;
|
||||||
|
metaTemplateName: string;
|
||||||
|
metaStatus: string;
|
||||||
|
language: string;
|
||||||
|
category: string;
|
||||||
|
content: string;
|
||||||
|
headerType?: string | null;
|
||||||
|
headerText?: string | null;
|
||||||
|
footerText?: string | null;
|
||||||
|
buttonsJson: string | null;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.database.query(
|
||||||
|
`INSERT INTO whatsapp_templates (
|
||||||
|
name, content, category, language,
|
||||||
|
meta_template_name, meta_template_id, meta_status,
|
||||||
|
header_type, header_text, footer_text, buttons, body_variables,
|
||||||
|
status, meta_approved_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, NULL,
|
||||||
|
CASE WHEN $7 = 'APPROVED' THEN 'approved' ELSE 'pending' END,
|
||||||
|
CASE WHEN $7 = 'APPROVED' THEN CURRENT_TIMESTAMP ELSE NULL END,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
ON CONFLICT (meta_template_name) WHERE meta_template_name IS NOT NULL DO NOTHING`,
|
||||||
|
[
|
||||||
|
input.metaTemplateName,
|
||||||
|
input.content,
|
||||||
|
input.category,
|
||||||
|
input.language,
|
||||||
|
input.metaTemplateName,
|
||||||
|
input.metaTemplateId,
|
||||||
|
input.metaStatus,
|
||||||
|
input.headerType || null,
|
||||||
|
input.headerText || null,
|
||||||
|
input.footerText || null,
|
||||||
|
input.buttonsJson,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return (result.rowCount || 0) > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deleteTemplate(id: number) {
|
deleteTemplate(id: number) {
|
||||||
return this.database.query('DELETE FROM whatsapp_templates WHERE id = $1 RETURNING meta_template_name', [id]);
|
return this.database.query('DELETE FROM whatsapp_templates WHERE id = $1 RETURNING meta_template_name', [id]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,12 +114,12 @@ export class WhatsappTemplateService {
|
|||||||
async syncFromMeta() {
|
async syncFromMeta() {
|
||||||
const config = await this.configService.getConfig();
|
const config = await this.configService.getConfig();
|
||||||
if (!config.access_token || !config.waba_id) {
|
if (!config.access_token || !config.waba_id) {
|
||||||
return { synced: 0, error: 'WhatsApp não configurado com WABA ID' };
|
return { synced: 0, imported: 0, error: 'WhatsApp não configurado com WABA ID' };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://graph.facebook.com/${config.api_version}/${config.waba_id}/message_templates?fields=id,name,status&limit=250`,
|
`https://graph.facebook.com/${config.api_version}/${config.waba_id}/message_templates?fields=id,name,status,language,category,components&limit=250`,
|
||||||
{ headers: { Authorization: `Bearer ${config.access_token}` } },
|
{ headers: { Authorization: `Bearer ${config.access_token}` } },
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -127,19 +127,44 @@ export class WhatsappTemplateService {
|
|||||||
|
|
||||||
if (!response.ok || !Array.isArray(data?.data)) {
|
if (!response.ok || !Array.isArray(data?.data)) {
|
||||||
this.logger.error('[Meta Templates] Erro ao buscar lista:', data?.error);
|
this.logger.error('[Meta Templates] Erro ao buscar lista:', data?.error);
|
||||||
return { synced: 0, error: data?.error?.message || 'Erro ao sincronizar' };
|
return { synced: 0, imported: 0, error: data?.error?.message || 'Erro ao sincronizar' };
|
||||||
}
|
}
|
||||||
|
|
||||||
let synced = 0;
|
let synced = 0;
|
||||||
|
let imported = 0;
|
||||||
|
|
||||||
for (const t of data.data) {
|
for (const t of data.data) {
|
||||||
const updated = await this.repo.updateMetaStatusByName(t.name, t.status, String(t.id));
|
const metaId = String(t.id);
|
||||||
if (updated > 0) synced++;
|
const updated = await this.repo.updateMetaStatusByName(t.name, t.status, metaId);
|
||||||
|
if (updated > 0) {
|
||||||
|
synced++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = this.parseComponents(t.components || []);
|
||||||
|
const wasInserted = await this.repo.importFromMeta({
|
||||||
|
metaTemplateId: metaId,
|
||||||
|
metaTemplateName: t.name,
|
||||||
|
metaStatus: t.status,
|
||||||
|
language: t.language || 'pt_BR',
|
||||||
|
category: this.normalizeCategory(t.category),
|
||||||
|
content: parsed.content,
|
||||||
|
headerType: parsed.headerType,
|
||||||
|
headerText: parsed.headerText,
|
||||||
|
footerText: parsed.footerText,
|
||||||
|
buttonsJson: parsed.buttons?.length ? JSON.stringify(parsed.buttons) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wasInserted) {
|
||||||
|
imported++;
|
||||||
|
this.logger.log(`[Meta Templates] Importado: "${t.name}" (${t.status})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { synced };
|
return { synced, imported };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error('[Meta Templates] Falha na sincronização:', err?.message);
|
this.logger.error('[Meta Templates] Falha na sincronização:', err?.message);
|
||||||
return { synced: 0, error: 'Erro de conexão com a Meta' };
|
return { synced: 0, imported: 0, error: 'Erro de conexão com a Meta' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +248,33 @@ export class WhatsappTemplateService {
|
|||||||
return components;
|
return components;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseComponents(components: any[]): {
|
||||||
|
content: string;
|
||||||
|
headerType?: string | null;
|
||||||
|
headerText?: string | null;
|
||||||
|
footerText?: string | null;
|
||||||
|
buttons?: any[];
|
||||||
|
} {
|
||||||
|
const result: any = { content: '', headerType: null, headerText: null, footerText: null, buttons: [] };
|
||||||
|
for (const c of components || []) {
|
||||||
|
if (c.type === 'BODY') result.content = c.text || '';
|
||||||
|
else if (c.type === 'HEADER') {
|
||||||
|
result.headerType = c.format || null;
|
||||||
|
result.headerText = c.format === 'TEXT' ? (c.text || null) : null;
|
||||||
|
}
|
||||||
|
else if (c.type === 'FOOTER') result.footerText = c.text || null;
|
||||||
|
else if (c.type === 'BUTTONS') {
|
||||||
|
result.buttons = (c.buttons || []).map((b: any) => ({
|
||||||
|
type: b.type,
|
||||||
|
text: b.text || '',
|
||||||
|
url: b.url || '',
|
||||||
|
phone: b.phone_number || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private toMetaName(displayName: string): string {
|
private toMetaName(displayName: string): string {
|
||||||
return String(displayName || '')
|
return String(displayName || '')
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user