Files
moreminimore-chat/app/services/whatsapp/template_processor_service.rb
Muhsin Keloth e1270a4ef8 fix(whatsapp): render template values in conversation transcript (#15255)
WhatsApp template messages can be delivered with the correct variable
values while the Chatwoot conversation shows numeric placeholders
instead. This affects template sends where the client submits the raw
template body together with valid processed parameters; WhatsApp
delivery itself remains unchanged.

The message model currently passes outgoing content through Liquid
before saving it. Liquid interprets positional WhatsApp placeholders
such as `{{1}}` and `{{2}}` as numeric expressions, even though the
WhatsApp sender independently uses `processed_params.body` to build the
provider payload.


Fixes
https://linear.app/chatwoot/issue/PLA-192/render-whatsapp-template-values-in-conversation-transcripts

### What changed

For WhatsApp template messages, Chatwoot now substitutes positional or
named body placeholders from `processed_params.body` before saving the
transcript. Missing values remain visible as placeholders, and ordinary
outgoing Liquid messages continue through the existing rendering path.

The existing message model regression suite remains green, and the
reported payload now persists as `Hello Ahmad, Furqan is your contact.`

### Things to know

This corrects newly created messages. Existing conversation messages
that were already stored as `1`, `2`, and so on are not backfilled.

### How to reproduce

1. Open a WhatsApp conversation outside the 24-hour messaging window.
2. Send a positional template whose body contains `{{1}}` and `{{2}}`.
3. Supply values for both parameters while submitting the original
template body as the message content.
4. Observe that WhatsApp delivers the substituted values, while the
Chatwoot transcript shows `1` and `2`.

### How to test

1. Select a WhatsApp template with two body variables.
2. Enter `Ahmad` and `Furqan` as the values and send the message.
3. Confirm the recipient receives the substituted template.
4. Confirm the Chatwoot conversation also displays `Ahmad` and `Furqan`
instead of the numeric placeholders.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-08-03 12:31:06 +04:00

135 lines
4.2 KiB
Ruby

class Whatsapp::TemplateProcessorService
pattr_initialize [:channel!, :template_params, :message]
def call
return [nil, nil, nil, nil] if template_params.blank?
process_template_with_params
end
private
def process_template_with_params
[
template_params['name'],
template_params['namespace'],
template_params['language'],
processed_templates_params
]
end
def find_template
channel.message_templates.find do |t|
t['name'] == template_params['name'] &&
t['language']&.downcase == template_params['language']&.downcase &&
t['status']&.downcase == 'approved'
end
end
def processed_templates_params
template = find_template
return if template.blank?
# Convert legacy format to enhanced format before processing
converter = Whatsapp::TemplateParameterConverterService.new(template_params, template)
normalized_params = converter.normalize_to_enhanced
process_enhanced_template_params(template, normalized_params['processed_params'])
end
def process_enhanced_template_params(template, processed_params = nil)
processed_params ||= template_params['processed_params']
components = []
components.concat(process_header_components(processed_params, template))
components.concat(process_body_components(processed_params, template))
components.concat(process_footer_components(processed_params))
components.concat(process_button_components(processed_params))
@template_params = components
end
def process_header_components(processed_params, template)
return [] if processed_params['header'].blank?
header_params = build_header_params(processed_params['header'], template)
header_params.present? ? [{ type: 'header', parameters: header_params }] : []
end
def build_header_params(header_data, template)
header_component = template['components']&.find { |component| component['type'] == 'HEADER' }
return build_text_header_params(header_data, template) if header_component&.dig('format') == 'TEXT'
build_media_header_params(header_data)
end
def build_text_header_params(header_data, template)
header_data.filter_map do |key, value|
build_text_parameter(key, value, template) if value.present?
end
end
def build_media_header_params(header_data)
return [] if header_data['media_url'].blank? || header_data['media_type'].blank?
media_param = parameter_builder.build_media_parameter(header_data['media_url'], header_data['media_type'], header_data['media_name'])
media_param ? [media_param] : []
end
def process_body_components(processed_params, template)
return [] if processed_params['body'].blank?
body_parameters = processed_params['body']
body_parameters = body_parameters.sort_by { |key, _value| key.to_i } unless template['parameter_format'] == 'NAMED'
body_params = body_parameters.filter_map do |key, value|
next if value.blank?
build_text_parameter(key, value, template)
end
body_params.present? ? [{ type: 'body', parameters: body_params }] : []
end
def build_text_parameter(key, value, template)
return parameter_builder.build_named_parameter(key, value) if template['parameter_format'] == 'NAMED'
parameter_builder.build_parameter(value)
end
def process_footer_components(processed_params)
return [] if processed_params['footer'].blank?
footer_params = processed_params['footer'].filter_map do |_, value|
next if value.blank?
parameter_builder.build_parameter(value)
end
footer_params.present? ? [{ type: 'footer', parameters: footer_params }] : []
end
def process_button_components(processed_params)
return [] if processed_params['buttons'].blank?
button_params = processed_params['buttons'].filter_map.with_index do |button, index|
next if button.blank?
if button['type'] == 'url' || button['parameter'].present?
{
type: 'button',
sub_type: button['type'] || 'url',
index: index,
parameters: [parameter_builder.build_button_parameter(button)]
}
end
end
button_params.compact
end
def parameter_builder
@parameter_builder ||= Whatsapp::PopulateTemplateParametersService.new
end
end