From 6231e6ecdacc8221b7b4cf11e31dd1f9f078146a Mon Sep 17 00:00:00 2001 From: sthirumalairajan2212 <92260025+sthirumalairajan2212@users.noreply.github.com> Date: Wed, 17 Jun 2026 06:15:41 +0000 Subject: [PATCH] fix(broadcasts): use template's own header image for media-template broadcasts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A media-header template stores its image in message_templates.header_media_library_id. The template test-send resolved the header from that column, but the broadcast path only looked at broadcast.media_library_id and never fetched the template's own image. When a user broadcast a media-header template, no header component was built and Meta rejected the send with #131008 (missing required parameter) — Pending then Failed, even though Send Test worked. Backend: fetch t.header_media_library_id in the /send and /test SELECTs and fall back to it (broadcast.media_library_id || template.header_media_library_id) when resolving the header media and stamping template_meta. Frontend: auto-prefill the broadcast media picker with the template's saved header image so users don't re-select it, the preview renders, and Send isn't blocked. Signed-off-by: sthirumalairajan2212 <92260025+sthirumalairajan2212@users.noreply.github.com> --- backend/src/routes/broadcasts.js | 34 +++++++++++++++++++------- frontend/src/pages/BulkMessagePage.jsx | 12 +++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/backend/src/routes/broadcasts.js b/backend/src/routes/broadcasts.js index 3c5fc7c..44561af 100644 --- a/backend/src/routes/broadcasts.js +++ b/backend/src/routes/broadcasts.js @@ -50,7 +50,7 @@ async function enqueueBroadcastRecipient({ broadcast, template, account, recipie header_text: template.header_text || null, // Stable pointer to the header image so the Chats bubble renders the // real picture instead of a grey "Image header" placeholder. - header_media_library_id: broadcast.media_library_id || null, + header_media_library_id: broadcast.media_library_id || (template && template.header_media_library_id) || null, footer: template.footer || null, buttons: Array.isArray(template.buttons) ? template.buttons : (template.buttons || []), }, @@ -425,7 +425,8 @@ router.post('/broadcasts/:id/send', requirePermission('bulk-message'), async (re try { const { rows: bRows } = await pool.query( `SELECT b.*, t.id AS t_id, t.name AS t_name, t.language AS t_language, t.body AS t_body, - t.header_type AS t_header_type, t.header_text AS t_header_text, t.footer AS t_footer, t.buttons AS t_buttons, t.samples AS t_samples + t.header_type AS t_header_type, t.header_text AS t_header_text, t.footer AS t_footer, t.buttons AS t_buttons, t.samples AS t_samples, + t.header_media_library_id AS t_header_media_library_id FROM coexistence.broadcasts b LEFT JOIN coexistence.message_templates t ON t.id = b.template_id WHERE b.id = $1`, @@ -435,7 +436,8 @@ router.post('/broadcasts/:id/send', requirePermission('bulk-message'), async (re const broadcast = bRows[0]; const template = broadcast.message_type === 'template' ? { id: broadcast.t_id, name: broadcast.t_name, language: broadcast.t_language, body: broadcast.t_body, - header_type: broadcast.t_header_type, header_text: broadcast.t_header_text, footer: broadcast.t_footer, buttons: broadcast.t_buttons, samples: broadcast.t_samples } + header_type: broadcast.t_header_type, header_text: broadcast.t_header_text, footer: broadcast.t_footer, buttons: broadcast.t_buttons, samples: broadcast.t_samples, + header_media_library_id: broadcast.t_header_media_library_id } : null; const { account, error } = await resolveAccount({ fromPhoneNumber: broadcast.from_number }); @@ -458,11 +460,17 @@ router.post('/broadcasts/:id/send', requirePermission('bulk-message'), async (re // broadcast.media_library_id. const _tplHt = template ? String(template.header_type || '').toUpperCase() : ''; const _needsHeaderMedia = broadcast.message_type === 'template' && ['IMAGE', 'VIDEO', 'DOCUMENT'].includes(_tplHt); - if ((['image', 'video', 'audio', 'document'].includes(broadcast.message_type) || _needsHeaderMedia) && broadcast.media_library_id) { + // A media-header template carries its own image in header_media_library_id. + // Fall back to it when the broadcast has no explicitly-attached media, so a + // template broadcast resolves the header exactly like a template test-send + // instead of failing at Meta with #131008 (missing required header param). + const mediaSourceId = broadcast.media_library_id + || (_needsHeaderMedia && template ? template.header_media_library_id : null); + if ((['image', 'video', 'audio', 'document'].includes(broadcast.message_type) || _needsHeaderMedia) && mediaSourceId) { const { syncMediaToAccount } = require('./mediaLibrary'); const { rows: mRows } = await pool.query( `SELECT * FROM coexistence.media_library WHERE id = $1 AND deleted_at IS NULL`, - [broadcast.media_library_id] + [mediaSourceId] ); if (mRows.length) { const media = mRows[0]; @@ -538,7 +546,8 @@ router.post('/broadcasts/:id/test', requirePermission('bulk-message'), async (re const { rows: bRows } = await pool.query( `SELECT b.*, t.id AS t_id, t.name AS t_name, t.language AS t_language, t.body AS t_body, - t.header_type AS t_header_type, t.header_text AS t_header_text, t.footer AS t_footer, t.buttons AS t_buttons, t.samples AS t_samples + t.header_type AS t_header_type, t.header_text AS t_header_text, t.footer AS t_footer, t.buttons AS t_buttons, t.samples AS t_samples, + t.header_media_library_id AS t_header_media_library_id FROM coexistence.broadcasts b LEFT JOIN coexistence.message_templates t ON t.id = b.template_id WHERE b.id = $1`, @@ -548,7 +557,8 @@ router.post('/broadcasts/:id/test', requirePermission('bulk-message'), async (re const broadcast = bRows[0]; const template = broadcast.message_type === 'template' ? { id: broadcast.t_id, name: broadcast.t_name, language: broadcast.t_language, body: broadcast.t_body, - header_type: broadcast.t_header_type, header_text: broadcast.t_header_text, footer: broadcast.t_footer, buttons: broadcast.t_buttons, samples: broadcast.t_samples } + header_type: broadcast.t_header_type, header_text: broadcast.t_header_text, footer: broadcast.t_footer, buttons: broadcast.t_buttons, samples: broadcast.t_samples, + header_media_library_id: broadcast.t_header_media_library_id } : null; const { account, error } = await resolveAccount({ fromPhoneNumber: broadcast.from_number }); @@ -561,11 +571,17 @@ router.post('/broadcasts/:id/test', requirePermission('bulk-message'), async (re // broadcast.media_library_id. const _tplHt = template ? String(template.header_type || '').toUpperCase() : ''; const _needsHeaderMedia = broadcast.message_type === 'template' && ['IMAGE', 'VIDEO', 'DOCUMENT'].includes(_tplHt); - if ((['image', 'video', 'audio', 'document'].includes(broadcast.message_type) || _needsHeaderMedia) && broadcast.media_library_id) { + // A media-header template carries its own image in header_media_library_id. + // Fall back to it when the broadcast has no explicitly-attached media, so a + // template broadcast resolves the header exactly like a template test-send + // instead of failing at Meta with #131008 (missing required header param). + const mediaSourceId = broadcast.media_library_id + || (_needsHeaderMedia && template ? template.header_media_library_id : null); + if ((['image', 'video', 'audio', 'document'].includes(broadcast.message_type) || _needsHeaderMedia) && mediaSourceId) { const { syncMediaToAccount } = require('./mediaLibrary'); const { rows: mRows } = await pool.query( `SELECT * FROM coexistence.media_library WHERE id = $1 AND deleted_at IS NULL`, - [broadcast.media_library_id] + [mediaSourceId] ); if (mRows.length) { const media = mRows[0]; diff --git a/frontend/src/pages/BulkMessagePage.jsx b/frontend/src/pages/BulkMessagePage.jsx index d8ed84e..4c7407f 100644 --- a/frontend/src/pages/BulkMessagePage.jsx +++ b/frontend/src/pages/BulkMessagePage.jsx @@ -430,6 +430,18 @@ export default function BulkMessagePage({ onNavigate }) { .then(res => { const filtered = (res.media || []).filter(m => m.mediaType === neededType); setNewBroadcastMediaItems(filtered); + // For a media-header template, auto-prefill the picker with the image the + // template was built with (its header_media_library_id), if it still + // exists in the library — so the user doesn't have to re-select it and a + // template broadcast works just like a template test-send. They can still + // override the choice. + if (newBroadcastMessageType === 'template') { + const tpl = templates.find(t => t.id.toString() === newBroadcastTemplateId); + const savedId = tpl?.header_media_library_id; + if (savedId && filtered.some(m => String(m.id) === String(savedId))) { + setNewBroadcastMediaLibraryId(prev => prev || String(savedId)); + } + } }) .catch(() => setNewBroadcastMediaItems([])) .finally(() => setNewBroadcastMediaLoading(false));