mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: Enhanced WhatsApp template support with media headers (#11997)
This commit is contained in:
148
app/services/whatsapp/populate_template_parameters_service.rb
Normal file
148
app/services/whatsapp/populate_template_parameters_service.rb
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
class Whatsapp::PopulateTemplateParametersService
|
||||||
|
def build_parameter(value)
|
||||||
|
case value
|
||||||
|
when String
|
||||||
|
build_string_parameter(value)
|
||||||
|
when Hash
|
||||||
|
build_hash_parameter(value)
|
||||||
|
else
|
||||||
|
{ type: 'text', text: value.to_s }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_button_parameter(button)
|
||||||
|
return { type: 'text', text: '' } if button.blank?
|
||||||
|
|
||||||
|
case button['type']
|
||||||
|
when 'copy_code'
|
||||||
|
coupon_code = button['parameter'].to_s.strip
|
||||||
|
raise ArgumentError, 'Coupon code cannot be empty' if coupon_code.blank?
|
||||||
|
raise ArgumentError, 'Coupon code cannot exceed 15 characters' if coupon_code.length > 15
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'coupon_code',
|
||||||
|
coupon_code: coupon_code
|
||||||
|
}
|
||||||
|
else
|
||||||
|
# For URL buttons and other button types, treat parameter as text
|
||||||
|
# If parameter is blank, use empty string (required for URL buttons)
|
||||||
|
{ type: 'text', text: button['parameter'].to_s.strip }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_media_parameter(url, media_type)
|
||||||
|
return nil if url.blank?
|
||||||
|
|
||||||
|
sanitized_url = sanitize_parameter(url)
|
||||||
|
validate_url(sanitized_url)
|
||||||
|
build_media_type_parameter(sanitized_url, media_type.downcase)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_named_parameter(parameter_name, value)
|
||||||
|
sanitized_value = sanitize_parameter(value.to_s)
|
||||||
|
{ type: 'text', parameter_name: parameter_name, text: sanitized_value }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_string_parameter(value)
|
||||||
|
sanitized_value = sanitize_parameter(value)
|
||||||
|
if rich_formatting?(sanitized_value)
|
||||||
|
build_rich_text_parameter(sanitized_value)
|
||||||
|
else
|
||||||
|
{ type: 'text', text: sanitized_value }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_hash_parameter(value)
|
||||||
|
case value['type']
|
||||||
|
when 'currency'
|
||||||
|
build_currency_parameter(value)
|
||||||
|
when 'date_time'
|
||||||
|
build_date_time_parameter(value)
|
||||||
|
else
|
||||||
|
{ type: 'text', text: value.to_s }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_currency_parameter(value)
|
||||||
|
{
|
||||||
|
type: 'currency',
|
||||||
|
currency: {
|
||||||
|
fallback_value: value['fallback_value'],
|
||||||
|
code: value['code'],
|
||||||
|
amount_1000: value['amount_1000']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_date_time_parameter(value)
|
||||||
|
{
|
||||||
|
type: 'date_time',
|
||||||
|
date_time: {
|
||||||
|
fallback_value: value['fallback_value'],
|
||||||
|
day_of_week: value['day_of_week'],
|
||||||
|
day_of_month: value['day_of_month'],
|
||||||
|
month: value['month'],
|
||||||
|
year: value['year']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_media_type_parameter(sanitized_url, media_type)
|
||||||
|
case media_type
|
||||||
|
when 'image'
|
||||||
|
build_image_parameter(sanitized_url)
|
||||||
|
when 'video'
|
||||||
|
build_video_parameter(sanitized_url)
|
||||||
|
when 'document'
|
||||||
|
build_document_parameter(sanitized_url)
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Unsupported media type: #{media_type}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_image_parameter(url)
|
||||||
|
{ type: 'image', image: { link: url } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_video_parameter(url)
|
||||||
|
{ type: 'video', video: { link: url } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_document_parameter(url)
|
||||||
|
{ type: 'document', document: { link: url } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def rich_formatting?(text)
|
||||||
|
# Check if text contains WhatsApp rich formatting markers
|
||||||
|
text.match?(/\*[^*]+\*/) || # Bold: *text*
|
||||||
|
text.match?(/_[^_]+_/) || # Italic: _text_
|
||||||
|
text.match?(/~[^~]+~/) || # Strikethrough: ~text~
|
||||||
|
text.match?(/```[^`]+```/) # Monospace: ```text```
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_rich_text_parameter(text)
|
||||||
|
# WhatsApp supports rich text formatting in templates
|
||||||
|
# This preserves the formatting markers for the API
|
||||||
|
{ type: 'text', text: text }
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitize_parameter(value)
|
||||||
|
# Basic sanitization - remove dangerous characters and limit length
|
||||||
|
sanitized = value.to_s.strip
|
||||||
|
sanitized = sanitized.gsub(/[<>\"']/, '') # Remove potential HTML/JS chars
|
||||||
|
sanitized[0...1000] # Limit length to prevent DoS
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_url(url)
|
||||||
|
return if url.blank?
|
||||||
|
|
||||||
|
uri = URI.parse(url)
|
||||||
|
raise ArgumentError, "Invalid URL scheme: #{uri.scheme}. Only http and https are allowed" unless %w[http https].include?(uri.scheme)
|
||||||
|
raise ArgumentError, 'URL too long (max 2000 characters)' if url.length > 2000
|
||||||
|
|
||||||
|
rescue URI::InvalidURIError => e
|
||||||
|
raise ArgumentError, "Invalid URL format: #{e.message}. Please enter a valid URL like https://example.com/document.pdf"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -106,10 +106,7 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
|
|||||||
policy: 'deterministic',
|
policy: 'deterministic',
|
||||||
code: template_info[:lang_code]
|
code: template_info[:lang_code]
|
||||||
},
|
},
|
||||||
components: [{
|
components: template_info[:parameters]
|
||||||
type: 'body',
|
|
||||||
parameters: template_info[:parameters]
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,15 +12,20 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_template(phone_number, template_info)
|
def send_template(phone_number, template_info)
|
||||||
|
template_body = template_body_parameters(template_info)
|
||||||
|
|
||||||
|
request_body = {
|
||||||
|
messaging_product: 'whatsapp',
|
||||||
|
recipient_type: 'individual', # Only individual messages supported (not group messages)
|
||||||
|
to: phone_number,
|
||||||
|
type: 'template',
|
||||||
|
template: template_body
|
||||||
|
}
|
||||||
|
|
||||||
response = HTTParty.post(
|
response = HTTParty.post(
|
||||||
"#{phone_id_path}/messages",
|
"#{phone_id_path}/messages",
|
||||||
headers: api_headers,
|
headers: api_headers,
|
||||||
body: {
|
body: request_body.to_json
|
||||||
messaging_product: 'whatsapp',
|
|
||||||
to: phone_number,
|
|
||||||
template: template_body_parameters(template_info),
|
|
||||||
type: 'template'
|
|
||||||
}.to_json
|
|
||||||
)
|
)
|
||||||
|
|
||||||
process_response(response)
|
process_response(response)
|
||||||
@@ -119,17 +124,36 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def template_body_parameters(template_info)
|
def template_body_parameters(template_info)
|
||||||
{
|
template_body = {
|
||||||
name: template_info[:name],
|
name: template_info[:name],
|
||||||
language: {
|
language: {
|
||||||
policy: 'deterministic',
|
policy: 'deterministic',
|
||||||
code: template_info[:lang_code]
|
code: template_info[:lang_code]
|
||||||
},
|
}
|
||||||
components: [{
|
|
||||||
type: 'body',
|
|
||||||
parameters: template_info[:parameters]
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Enhanced template parameters structure
|
||||||
|
# Note: Legacy format support (simple parameter arrays) has been removed
|
||||||
|
# in favor of the enhanced component-based structure that supports
|
||||||
|
# headers, buttons, and authentication templates.
|
||||||
|
#
|
||||||
|
# Expected payload format from frontend:
|
||||||
|
# {
|
||||||
|
# processed_params: {
|
||||||
|
# body: { '1': 'John', '2': '123 Main St' },
|
||||||
|
# header: { media_url: 'https://...', media_type: 'image' },
|
||||||
|
# buttons: [{ type: 'url', parameter: 'otp123456' }]
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# This gets transformed into WhatsApp API component format:
|
||||||
|
# [
|
||||||
|
# { type: 'body', parameters: [...] },
|
||||||
|
# { type: 'header', parameters: [...] },
|
||||||
|
# { type: 'button', sub_type: 'url', parameters: [...] }
|
||||||
|
# ]
|
||||||
|
template_body[:components] = template_info[:parameters] || []
|
||||||
|
|
||||||
|
template_body
|
||||||
end
|
end
|
||||||
|
|
||||||
def whatsapp_reply_context(message)
|
def whatsapp_reply_context(message)
|
||||||
|
|||||||
117
app/services/whatsapp/template_parameter_converter_service.rb
Normal file
117
app/services/whatsapp/template_parameter_converter_service.rb
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Service to convert legacy WhatsApp template parameter formats to enhanced format
|
||||||
|
#
|
||||||
|
# Legacy formats (deprecated):
|
||||||
|
# - Array: ["John", "Order123"] - positional parameters
|
||||||
|
# - Flat Hash: {"1": "John", "2": "Order123"} - direct key-value mapping
|
||||||
|
#
|
||||||
|
# Enhanced format:
|
||||||
|
# - Component-based: {"body": {"1": "John", "2": "Order123"}} - structured by template components
|
||||||
|
# - Supports header, body, footer, and button parameters separately
|
||||||
|
#
|
||||||
|
class Whatsapp::TemplateParameterConverterService
|
||||||
|
def initialize(template_params, template)
|
||||||
|
@template_params = template_params
|
||||||
|
@template = template
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_to_enhanced
|
||||||
|
processed_params = @template_params['processed_params']
|
||||||
|
|
||||||
|
# Early return if already enhanced format
|
||||||
|
return @template_params if enhanced_format?(processed_params)
|
||||||
|
|
||||||
|
# Mark as legacy format before conversion for tracking
|
||||||
|
@template_params['format_version'] = 'legacy'
|
||||||
|
|
||||||
|
# Convert legacy formats to enhanced structure
|
||||||
|
# TODO: Legacy format support will be deprecated and removed after 2-3 releases
|
||||||
|
enhanced_params = convert_legacy_to_enhanced(processed_params, @template)
|
||||||
|
|
||||||
|
# Replace original params with enhanced structure
|
||||||
|
@template_params['processed_params'] = enhanced_params
|
||||||
|
|
||||||
|
@template_params
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def enhanced_format?(processed_params)
|
||||||
|
return false unless processed_params.is_a?(Hash)
|
||||||
|
|
||||||
|
# Enhanced format has component-based structure
|
||||||
|
component_keys = %w[body header footer buttons]
|
||||||
|
has_component_structure = processed_params.keys.any? { |k| component_keys.include?(k) }
|
||||||
|
|
||||||
|
# Additional validation for enhanced format
|
||||||
|
if has_component_structure
|
||||||
|
validate_enhanced_structure(processed_params)
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_enhanced_structure(params)
|
||||||
|
valid_body?(params['body']) &&
|
||||||
|
valid_header?(params['header']) &&
|
||||||
|
valid_buttons?(params['buttons'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_body?(body)
|
||||||
|
body.nil? || body.is_a?(Hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_header?(header)
|
||||||
|
header.nil? || header.is_a?(Hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_buttons?(buttons)
|
||||||
|
return true if buttons.nil?
|
||||||
|
return false unless buttons.is_a?(Array)
|
||||||
|
|
||||||
|
buttons.all? { |b| b.is_a?(Hash) && b['type'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_legacy_to_enhanced(legacy_params, _template)
|
||||||
|
# Legacy system only supported text-based templates with body parameters
|
||||||
|
# We only convert the parameter format, not add new features
|
||||||
|
|
||||||
|
enhanced = {}
|
||||||
|
|
||||||
|
case legacy_params
|
||||||
|
when Array
|
||||||
|
# Array format: ["John", "Order123"] → {body: {"1": "John", "2": "Order123"}}
|
||||||
|
body_params = convert_array_to_body_params(legacy_params)
|
||||||
|
enhanced['body'] = body_params unless body_params.empty?
|
||||||
|
when Hash
|
||||||
|
# Hash format: {"1": "John", "name": "Jane"} → {body: {"1": "John", "name": "Jane"}}
|
||||||
|
body_params = convert_hash_to_body_params(legacy_params)
|
||||||
|
enhanced['body'] = body_params unless body_params.empty?
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Unknown legacy format: #{legacy_params.class}"
|
||||||
|
end
|
||||||
|
|
||||||
|
enhanced
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_array_to_body_params(params_array)
|
||||||
|
return {} if params_array.empty?
|
||||||
|
|
||||||
|
body_params = {}
|
||||||
|
params_array.each_with_index do |value, index|
|
||||||
|
body_params[(index + 1).to_s] = value.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
body_params
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_hash_to_body_params(params_hash)
|
||||||
|
return {} if params_hash.empty?
|
||||||
|
|
||||||
|
body_params = {}
|
||||||
|
params_hash.each do |key, value|
|
||||||
|
body_params[key.to_s] = value.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
body_params
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,11 +2,9 @@ class Whatsapp::TemplateProcessorService
|
|||||||
pattr_initialize [:channel!, :template_params, :message]
|
pattr_initialize [:channel!, :template_params, :message]
|
||||||
|
|
||||||
def call
|
def call
|
||||||
if template_params.present?
|
return [nil, nil, nil, nil] if template_params.blank?
|
||||||
process_template_with_params
|
|
||||||
else
|
process_template_with_params
|
||||||
process_template_from_message
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -20,51 +18,6 @@ class Whatsapp::TemplateProcessorService
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_template_from_message
|
|
||||||
return [nil, nil, nil, nil] if message.blank?
|
|
||||||
|
|
||||||
# Delete the following logic once the update for template_params is stable
|
|
||||||
# see if we can match the message content to a template
|
|
||||||
# An example template may look like "Your package has been shipped. It will be delivered in {{1}} business days.
|
|
||||||
# We want to iterate over these templates with our message body and see if we can fit it to any of the templates
|
|
||||||
# Then we use regex to parse the template varibles and convert them into the proper payload
|
|
||||||
channel.message_templates&.each do |template|
|
|
||||||
match_obj = template_match_object(template)
|
|
||||||
next if match_obj.blank?
|
|
||||||
|
|
||||||
# we have a match, now we need to parse the template variables and convert them into the wa recommended format
|
|
||||||
processed_parameters = match_obj.captures.map { |x| { type: 'text', text: x } }
|
|
||||||
|
|
||||||
# no need to look up further end the search
|
|
||||||
return [template['name'], template['namespace'], template['language'], processed_parameters]
|
|
||||||
end
|
|
||||||
[nil, nil, nil, nil]
|
|
||||||
end
|
|
||||||
|
|
||||||
def template_match_object(template)
|
|
||||||
body_object = validated_body_object(template)
|
|
||||||
return if body_object.blank?
|
|
||||||
|
|
||||||
template_match_regex = build_template_match_regex(body_object['text'])
|
|
||||||
message.outgoing_content.match(template_match_regex)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_template_match_regex(template_text)
|
|
||||||
# Converts the whatsapp template to a comparable regex string to check against the message content
|
|
||||||
# the variables are of the format {{num}} ex:{{1}}
|
|
||||||
|
|
||||||
# transform the template text into a regex string
|
|
||||||
# we need to replace the {{num}} with matchers that can be used to capture the variables
|
|
||||||
template_text = template_text.gsub(/{{\d}}/, '(.*)')
|
|
||||||
# escape if there are regex characters in the template text
|
|
||||||
template_text = Regexp.escape(template_text)
|
|
||||||
# ensuring only the variables remain as capture groups
|
|
||||||
template_text = template_text.gsub(Regexp.escape('(.*)'), '(.*)')
|
|
||||||
|
|
||||||
template_match_string = "^#{template_text}$"
|
|
||||||
Regexp.new template_match_string
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_template
|
def find_template
|
||||||
channel.message_templates.find do |t|
|
channel.message_templates.find do |t|
|
||||||
t['name'] == template_params['name'] && t['language'] == template_params['language'] && t['status']&.downcase == 'approved'
|
t['name'] == template_params['name'] && t['language'] == template_params['language'] && t['status']&.downcase == 'approved'
|
||||||
@@ -75,21 +28,100 @@ class Whatsapp::TemplateProcessorService
|
|||||||
template = find_template
|
template = find_template
|
||||||
return if template.blank?
|
return if template.blank?
|
||||||
|
|
||||||
parameter_format = template['parameter_format']
|
# Convert legacy format to enhanced format before processing
|
||||||
|
converter = Whatsapp::TemplateParameterConverterService.new(template_params, template)
|
||||||
|
normalized_params = converter.normalize_to_enhanced
|
||||||
|
|
||||||
if parameter_format == 'NAMED'
|
process_enhanced_template_params(template, normalized_params['processed_params'])
|
||||||
template_params['processed_params']&.map { |key, value| { type: 'text', parameter_name: key, text: value } }
|
|
||||||
else
|
|
||||||
template_params['processed_params']&.map { |_, value| { type: 'text', text: value } }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def validated_body_object(template)
|
def process_enhanced_template_params(template, processed_params = nil)
|
||||||
# we don't care if its not approved template
|
processed_params ||= template_params['processed_params']
|
||||||
return if template['status'] != 'approved'
|
components = []
|
||||||
|
|
||||||
# we only care about text body object in template. if not present we discard the template
|
components.concat(process_header_components(processed_params))
|
||||||
# we don't support other forms of templates
|
components.concat(process_body_components(processed_params, template))
|
||||||
template['components'].find { |obj| obj['type'] == 'BODY' && obj.key?('text') }
|
components.concat(process_footer_components(processed_params))
|
||||||
|
components.concat(process_button_components(processed_params))
|
||||||
|
|
||||||
|
@template_params = components
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_header_components(processed_params)
|
||||||
|
return [] if processed_params['header'].blank?
|
||||||
|
|
||||||
|
header_params = build_header_params(processed_params['header'])
|
||||||
|
header_params.present? ? [{ type: 'header', parameters: header_params }] : []
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_header_params(header_data)
|
||||||
|
header_params = []
|
||||||
|
header_data.each do |key, value|
|
||||||
|
next if value.blank?
|
||||||
|
|
||||||
|
if media_url_with_type?(key, header_data)
|
||||||
|
media_param = parameter_builder.build_media_parameter(value, header_data['media_type'])
|
||||||
|
header_params << media_param if media_param
|
||||||
|
elsif key != 'media_type'
|
||||||
|
header_params << parameter_builder.build_parameter(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
header_params
|
||||||
|
end
|
||||||
|
|
||||||
|
def media_url_with_type?(key, header_data)
|
||||||
|
key == 'media_url' && header_data['media_type'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_body_components(processed_params, template)
|
||||||
|
return [] if processed_params['body'].blank?
|
||||||
|
|
||||||
|
body_params = processed_params['body'].filter_map do |key, value|
|
||||||
|
next if value.blank?
|
||||||
|
|
||||||
|
parameter_format = template['parameter_format']
|
||||||
|
if parameter_format == 'NAMED'
|
||||||
|
parameter_builder.build_named_parameter(key, value)
|
||||||
|
else
|
||||||
|
parameter_builder.build_parameter(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
body_params.present? ? [{ type: 'body', parameters: body_params }] : []
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ describe Whatsapp::OneoffCampaignService do
|
|||||||
'namespace' => '23423423_2342423_324234234_2343224',
|
'namespace' => '23423423_2342423_324234234_2343224',
|
||||||
'category' => 'UTILITY',
|
'category' => 'UTILITY',
|
||||||
'language' => 'en',
|
'language' => 'en',
|
||||||
'processed_params' => { 'name' => 'John', 'ticket_id' => '2332' }
|
'processed_params' => { 'body' => { 'name' => 'John', 'ticket_id' => '2332' } }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -125,8 +125,13 @@ describe Whatsapp::OneoffCampaignService do
|
|||||||
namespace: '23423423_2342423_324234234_2343224',
|
namespace: '23423423_2342423_324234234_2343224',
|
||||||
lang_code: 'en',
|
lang_code: 'en',
|
||||||
parameters: array_including(
|
parameters: array_including(
|
||||||
hash_including(type: 'text', parameter_name: 'name', text: 'John'),
|
hash_including(
|
||||||
hash_including(type: 'text', parameter_name: 'ticket_id', text: '2332')
|
type: 'body',
|
||||||
|
parameters: array_including(
|
||||||
|
hash_including(type: 'text', parameter_name: 'name', text: 'John'),
|
||||||
|
hash_including(type: 'text', parameter_name: 'ticket_id', text: '2332')
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -165,19 +165,17 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
|||||||
let(:template_body) do
|
let(:template_body) do
|
||||||
{
|
{
|
||||||
messaging_product: 'whatsapp',
|
messaging_product: 'whatsapp',
|
||||||
|
recipient_type: 'individual', # Added recipient_type field
|
||||||
to: '+123456789',
|
to: '+123456789',
|
||||||
|
type: 'template',
|
||||||
template: {
|
template: {
|
||||||
name: template_info[:name],
|
name: template_info[:name],
|
||||||
language: {
|
language: {
|
||||||
policy: 'deterministic',
|
policy: 'deterministic',
|
||||||
code: template_info[:lang_code]
|
code: template_info[:lang_code]
|
||||||
},
|
},
|
||||||
components: [
|
components: template_info[:parameters] # Changed to use parameters directly (enhanced format)
|
||||||
{ type: 'body',
|
}
|
||||||
parameters: template_info[:parameters] }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
type: 'template'
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ describe Whatsapp::SendOnWhatsappService do
|
|||||||
namespace: '23423423_2342423_324234234_2343224',
|
namespace: '23423423_2342423_324234234_2343224',
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
category: 'Marketing',
|
category: 'Marketing',
|
||||||
processed_params: { '1' => '3' }
|
processed_params: { 'body' => { '1' => '3' } }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
@@ -39,15 +39,16 @@ describe Whatsapp::SendOnWhatsappService do
|
|||||||
let(:named_template_body) do
|
let(:named_template_body) do
|
||||||
{
|
{
|
||||||
messaging_product: 'whatsapp',
|
messaging_product: 'whatsapp',
|
||||||
|
recipient_type: 'individual',
|
||||||
to: '123456789',
|
to: '123456789',
|
||||||
|
type: 'template',
|
||||||
template: {
|
template: {
|
||||||
name: 'ticket_status_updated',
|
name: 'ticket_status_updated',
|
||||||
language: { 'policy': 'deterministic', 'code': 'en_US' },
|
language: { 'policy': 'deterministic', 'code': 'en_US' },
|
||||||
components: [{ 'type': 'body',
|
components: [{ 'type': 'body',
|
||||||
'parameters': [{ 'type': 'text', parameter_name: 'last_name', 'text': 'Dale' },
|
'parameters': [{ 'type': 'text', parameter_name: 'last_name', 'text': 'Dale' },
|
||||||
{ 'type': 'text', parameter_name: 'ticket_id', 'text': '2332' }] }]
|
{ 'type': 'text', parameter_name: 'ticket_id', 'text': '2332' }] }]
|
||||||
},
|
}
|
||||||
type: 'template'
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ describe Whatsapp::SendOnWhatsappService do
|
|||||||
|
|
||||||
it 'calls channel.send_template when after 24 hour limit' do
|
it 'calls channel.send_template when after 24 hour limit' do
|
||||||
message = create(:message, message_type: :outgoing, content: 'Your package has been shipped. It will be delivered in 3 business days.',
|
message = create(:message, message_type: :outgoing, content: 'Your package has been shipped. It will be delivered in 3 business days.',
|
||||||
conversation: conversation)
|
conversation: conversation, additional_attributes: { template_params: template_params })
|
||||||
|
|
||||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||||
.with(
|
.with(
|
||||||
@@ -107,12 +108,18 @@ describe Whatsapp::SendOnWhatsappService do
|
|||||||
name: 'ticket_status_updated',
|
name: 'ticket_status_updated',
|
||||||
language: 'en_US',
|
language: 'en_US',
|
||||||
category: 'UTILITY',
|
category: 'UTILITY',
|
||||||
processed_params: { 'last_name' => 'Dale', 'ticket_id' => '2332' }
|
processed_params: { 'body' => { 'last_name' => 'Dale', 'ticket_id' => '2332' } }
|
||||||
}
|
}
|
||||||
|
|
||||||
stub_request(:post, "https://graph.facebook.com/v13.0/#{whatsapp_cloud_channel.provider_config['phone_number_id']}/messages")
|
stub_request(:post, "https://graph.facebook.com/v13.0/#{whatsapp_cloud_channel.provider_config['phone_number_id']}/messages")
|
||||||
.with(
|
.with(
|
||||||
:headers => { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{whatsapp_cloud_channel.provider_config['api_key']}" },
|
:headers => {
|
||||||
|
'Accept' => '*/*',
|
||||||
|
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Authorization' => "Bearer #{whatsapp_cloud_channel.provider_config['api_key']}",
|
||||||
|
'User-Agent' => 'Ruby'
|
||||||
|
},
|
||||||
:body => named_template_body.to_json
|
:body => named_template_body.to_json
|
||||||
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||||
message = create(:message,
|
message = create(:message,
|
||||||
@@ -124,12 +131,44 @@ describe Whatsapp::SendOnWhatsappService do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'calls channel.send_template when template has regexp characters' do
|
it 'calls channel.send_template when template has regexp characters' do
|
||||||
message = create(
|
regexp_template_params = build_template_params('customer_yes_no', '2342384942_32423423_23423fdsdaf23', 'ar', {})
|
||||||
:message,
|
arabic_content = 'عميلنا العزيز الرجاء الرد على هذه الرسالة بكلمة *نعم* للرد على إستفساركم من قبل خدمة العملاء.'
|
||||||
message_type: :outgoing,
|
message = create_message_with_template(arabic_content, regexp_template_params)
|
||||||
content: 'عميلنا العزيز الرجاء الرد على هذه الرسالة بكلمة *نعم* للرد على إستفساركم من قبل خدمة العملاء.',
|
stub_template_request(regexp_template_params, [])
|
||||||
conversation: conversation
|
|
||||||
)
|
described_class.new(message: message).perform
|
||||||
|
expect(message.reload.source_id).to eq('123456789')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles template with header parameters' do
|
||||||
|
processed_params = {
|
||||||
|
'body' => { '1' => '3' },
|
||||||
|
'header' => { 'media_url' => 'https://example.com/image.jpg', 'media_type' => 'image' }
|
||||||
|
}
|
||||||
|
header_template_params = build_sample_template_params(processed_params)
|
||||||
|
message = create_message_with_template('', header_template_params)
|
||||||
|
|
||||||
|
components = [
|
||||||
|
{ type: 'header', parameters: [{ type: 'image', image: { link: 'https://example.com/image.jpg' } }] },
|
||||||
|
{ type: 'body', parameters: [{ type: 'text', text: '3' }] }
|
||||||
|
]
|
||||||
|
stub_sample_template_request(components)
|
||||||
|
|
||||||
|
described_class.new(message: message).perform
|
||||||
|
expect(message.reload.source_id).to eq('123456789')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles empty processed_params gracefully' do
|
||||||
|
empty_template_params = {
|
||||||
|
name: 'sample_shipping_confirmation',
|
||||||
|
namespace: '23423423_2342423_324234234_2343224',
|
||||||
|
language: 'en_US',
|
||||||
|
category: 'SHIPPING_UPDATE',
|
||||||
|
processed_params: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message = create(:message, additional_attributes: { template_params: empty_template_params },
|
||||||
|
conversation: conversation, message_type: :outgoing)
|
||||||
|
|
||||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||||
.with(
|
.with(
|
||||||
@@ -137,10 +176,10 @@ describe Whatsapp::SendOnWhatsappService do
|
|||||||
body: {
|
body: {
|
||||||
to: '123456789',
|
to: '123456789',
|
||||||
template: {
|
template: {
|
||||||
name: 'customer_yes_no',
|
name: 'sample_shipping_confirmation',
|
||||||
namespace: '2342384942_32423423_23423fdsdaf23',
|
namespace: '23423423_2342423_324234234_2343224',
|
||||||
language: { 'policy': 'deterministic', 'code': 'ar' },
|
language: { 'policy': 'deterministic', 'code': 'en_US' },
|
||||||
components: [{ 'type': 'body', 'parameters': [] }]
|
components: []
|
||||||
},
|
},
|
||||||
type: 'template'
|
type: 'template'
|
||||||
}.to_json
|
}.to_json
|
||||||
@@ -149,6 +188,169 @@ describe Whatsapp::SendOnWhatsappService do
|
|||||||
described_class.new(message: message).perform
|
described_class.new(message: message).perform
|
||||||
expect(message.reload.source_id).to eq('123456789')
|
expect(message.reload.source_id).to eq('123456789')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'handles template with button parameters' do
|
||||||
|
processed_params = {
|
||||||
|
'body' => { '1' => '3' },
|
||||||
|
'buttons' => [{ 'type' => 'url', 'parameter' => 'https://track.example.com/123' }]
|
||||||
|
}
|
||||||
|
button_template_params = build_sample_template_params(processed_params)
|
||||||
|
message = create_message_with_template('', button_template_params)
|
||||||
|
|
||||||
|
components = [
|
||||||
|
{ type: 'body', parameters: [{ type: 'text', text: '3' }] },
|
||||||
|
{ type: 'button', sub_type: 'url', index: 0, parameters: [{ type: 'text', text: 'https://track.example.com/123' }] }
|
||||||
|
]
|
||||||
|
stub_sample_template_request(components)
|
||||||
|
|
||||||
|
described_class.new(message: message).perform
|
||||||
|
expect(message.reload.source_id).to eq('123456789')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'processes template parameters correctly via integration' do
|
||||||
|
processed_params = {
|
||||||
|
'body' => { '1' => '5' },
|
||||||
|
'footer' => { 'text' => 'Thank you' }
|
||||||
|
}
|
||||||
|
complex_template_params = build_sample_template_params(processed_params)
|
||||||
|
message = create_message_with_template('', complex_template_params)
|
||||||
|
|
||||||
|
components = [
|
||||||
|
{ type: 'body', parameters: [{ type: 'text', text: '5' }] },
|
||||||
|
{ type: 'footer', parameters: [{ type: 'text', text: 'Thank you' }] }
|
||||||
|
]
|
||||||
|
stub_sample_template_request(components)
|
||||||
|
|
||||||
|
expect { described_class.new(message: message).perform }.not_to raise_error
|
||||||
|
expect(message.reload.source_id).to eq('123456789')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles edge case with missing template gracefully' do
|
||||||
|
# Test the service behavior when template is not found
|
||||||
|
missing_template_params = {
|
||||||
|
'name' => 'non_existent_template',
|
||||||
|
'namespace' => 'missing_namespace',
|
||||||
|
'language' => 'en_US',
|
||||||
|
'category' => 'UTILITY',
|
||||||
|
'processed_params' => { 'body' => { '1' => 'test' } }
|
||||||
|
}
|
||||||
|
|
||||||
|
service = Whatsapp::TemplateProcessorService.new(
|
||||||
|
channel: whatsapp_channel,
|
||||||
|
template_params: missing_template_params
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { service.call }.not_to raise_error
|
||||||
|
name, namespace, language, processed_params = service.call
|
||||||
|
expect(name).to eq('non_existent_template')
|
||||||
|
expect(namespace).to eq('missing_namespace')
|
||||||
|
expect(language).to eq('en_US')
|
||||||
|
expect(processed_params).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles template with blank parameter values correctly' do
|
||||||
|
processed_params = {
|
||||||
|
'body' => { '1' => '', '2' => 'valid_value', '3' => nil },
|
||||||
|
'header' => { 'media_url' => '', 'media_type' => 'image' }
|
||||||
|
}
|
||||||
|
blank_values_template_params = build_sample_template_params(processed_params)
|
||||||
|
message = create_message_with_template('', blank_values_template_params)
|
||||||
|
|
||||||
|
components = [{ type: 'body', parameters: [{ type: 'text', text: 'valid_value' }] }]
|
||||||
|
stub_sample_template_request(components)
|
||||||
|
|
||||||
|
described_class.new(message: message).perform
|
||||||
|
expect(message.reload.source_id).to eq('123456789')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles nil template_params gracefully' do
|
||||||
|
# Test service behavior when template_params is completely nil
|
||||||
|
message = create(:message, additional_attributes: {},
|
||||||
|
conversation: conversation, message_type: :outgoing)
|
||||||
|
|
||||||
|
# Should send regular message, not template
|
||||||
|
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||||
|
.with(
|
||||||
|
headers: headers,
|
||||||
|
body: {
|
||||||
|
to: '123456789',
|
||||||
|
text: { body: message.content },
|
||||||
|
type: 'text'
|
||||||
|
}.to_json
|
||||||
|
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||||
|
|
||||||
|
expect { described_class.new(message: message).perform }.not_to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'processes template with rich text formatting' do
|
||||||
|
processed_params = { 'body' => { '1' => '*Bold text* and _italic text_' } }
|
||||||
|
rich_text_template_params = build_sample_template_params(processed_params)
|
||||||
|
message = create_message_with_template('', rich_text_template_params)
|
||||||
|
|
||||||
|
components = [{ type: 'body', parameters: [{ type: 'text', text: '*Bold text* and _italic text_' }] }]
|
||||||
|
stub_sample_template_request(components)
|
||||||
|
|
||||||
|
described_class.new(message: message).perform
|
||||||
|
expect(message.reload.source_id).to eq('123456789')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_template_params(name, namespace, language, processed_params)
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
namespace: namespace,
|
||||||
|
language: language,
|
||||||
|
category: 'SHIPPING_UPDATE',
|
||||||
|
processed_params: processed_params
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_message_with_template(content, template_params)
|
||||||
|
create(:message,
|
||||||
|
message_type: :outgoing,
|
||||||
|
content: content,
|
||||||
|
conversation: conversation,
|
||||||
|
additional_attributes: { template_params: template_params })
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_template_request(template_params, components)
|
||||||
|
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||||
|
.with(
|
||||||
|
headers: headers,
|
||||||
|
body: {
|
||||||
|
to: '123456789',
|
||||||
|
template: {
|
||||||
|
name: template_params[:name],
|
||||||
|
namespace: template_params[:namespace],
|
||||||
|
language: { 'policy': 'deterministic', 'code': template_params[:language] },
|
||||||
|
components: components
|
||||||
|
},
|
||||||
|
type: 'template'
|
||||||
|
}.to_json
|
||||||
|
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_sample_template_params(processed_params)
|
||||||
|
build_template_params('sample_shipping_confirmation', '23423423_2342423_324234234_2343224', 'en_US', processed_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_sample_template_request(components)
|
||||||
|
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||||
|
.with(
|
||||||
|
headers: headers,
|
||||||
|
body: {
|
||||||
|
to: '123456789',
|
||||||
|
template: {
|
||||||
|
name: 'sample_shipping_confirmation',
|
||||||
|
namespace: '23423423_2342423_324234234_2343224',
|
||||||
|
language: { 'policy': 'deterministic', 'code': 'en_US' },
|
||||||
|
components: components
|
||||||
|
},
|
||||||
|
type: 'template'
|
||||||
|
}.to_json
|
||||||
|
).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Whatsapp::TemplateParameterConverterService do
|
||||||
|
let(:template) do
|
||||||
|
{
|
||||||
|
'name' => 'test_template',
|
||||||
|
'language' => 'en',
|
||||||
|
'components' => [
|
||||||
|
{
|
||||||
|
'type' => 'BODY',
|
||||||
|
'text' => 'Hello {{1}}, your order {{2}} is ready!'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:media_template) do
|
||||||
|
{
|
||||||
|
'name' => 'media_template',
|
||||||
|
'language' => 'en',
|
||||||
|
'components' => [
|
||||||
|
{
|
||||||
|
'type' => 'HEADER',
|
||||||
|
'format' => 'IMAGE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type' => 'BODY',
|
||||||
|
'text' => 'Check out {{1}}!'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:button_template) do
|
||||||
|
{
|
||||||
|
'name' => 'button_template',
|
||||||
|
'language' => 'en',
|
||||||
|
'components' => [
|
||||||
|
{
|
||||||
|
'type' => 'BODY',
|
||||||
|
'text' => 'Visit our website!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type' => 'BUTTONS',
|
||||||
|
'buttons' => [
|
||||||
|
{
|
||||||
|
'type' => 'URL',
|
||||||
|
'url' => 'https://example.com/{{1}}'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type' => 'COPY_CODE'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#normalize_to_enhanced' do
|
||||||
|
context 'when already enhanced format' do
|
||||||
|
let(:enhanced_params) do
|
||||||
|
{
|
||||||
|
'processed_params' => {
|
||||||
|
'body' => { '1' => 'John', '2' => 'Order123' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns unchanged' do
|
||||||
|
converter = described_class.new(enhanced_params, template)
|
||||||
|
result = converter.normalize_to_enhanced
|
||||||
|
expect(result).to eq(enhanced_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when legacy array format' do
|
||||||
|
let(:legacy_array_params) do
|
||||||
|
{
|
||||||
|
'processed_params' => %w[John Order123]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts to enhanced format' do
|
||||||
|
converter = described_class.new(legacy_array_params, template)
|
||||||
|
result = converter.normalize_to_enhanced
|
||||||
|
|
||||||
|
expect(result['processed_params']).to eq({
|
||||||
|
'body' => { '1' => 'John', '2' => 'Order123' }
|
||||||
|
})
|
||||||
|
expect(result['format_version']).to eq('legacy')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when legacy flat hash format' do
|
||||||
|
let(:legacy_hash_params) do
|
||||||
|
{
|
||||||
|
'processed_params' => { '1' => 'John', '2' => 'Order123' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts to enhanced format' do
|
||||||
|
converter = described_class.new(legacy_hash_params, template)
|
||||||
|
result = converter.normalize_to_enhanced
|
||||||
|
|
||||||
|
expect(result['processed_params']).to eq({
|
||||||
|
'body' => { '1' => 'John', '2' => 'Order123' }
|
||||||
|
})
|
||||||
|
expect(result['format_version']).to eq('legacy')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when legacy hash with all body parameters' do
|
||||||
|
let(:legacy_hash_params) do
|
||||||
|
{
|
||||||
|
'processed_params' => {
|
||||||
|
'1' => 'Product',
|
||||||
|
'customer_name' => 'John'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts to enhanced format with body only' do
|
||||||
|
converter = described_class.new(legacy_hash_params, media_template)
|
||||||
|
result = converter.normalize_to_enhanced
|
||||||
|
|
||||||
|
expect(result['processed_params']).to eq({
|
||||||
|
'body' => {
|
||||||
|
'1' => 'Product',
|
||||||
|
'customer_name' => 'John'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(result['format_version']).to eq('legacy')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when invalid format' do
|
||||||
|
let(:invalid_params) do
|
||||||
|
{
|
||||||
|
'processed_params' => 'invalid_string'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises ArgumentError' do
|
||||||
|
expect do
|
||||||
|
converter = described_class.new(invalid_params, template)
|
||||||
|
converter.normalize_to_enhanced
|
||||||
|
end.to raise_error(ArgumentError, /Unknown legacy format/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#enhanced_format?' do
|
||||||
|
it 'returns true for valid enhanced format' do
|
||||||
|
enhanced = { 'body' => { '1' => 'test' } }
|
||||||
|
converter = described_class.new({}, template)
|
||||||
|
expect(converter.send(:enhanced_format?, enhanced)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for array' do
|
||||||
|
converter = described_class.new({}, template)
|
||||||
|
expect(converter.send(:enhanced_format?, ['test'])).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for flat hash' do
|
||||||
|
converter = described_class.new({}, template)
|
||||||
|
expect(converter.send(:enhanced_format?, { '1' => 'test' })).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false for invalid structure' do
|
||||||
|
invalid = { 'body' => 'not_a_hash' }
|
||||||
|
converter = described_class.new({}, template)
|
||||||
|
expect(converter.send(:enhanced_format?, invalid)).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'simplified conversion methods' do
|
||||||
|
describe '#convert_array_to_body_params' do
|
||||||
|
it 'converts empty array' do
|
||||||
|
converter = described_class.new({}, template)
|
||||||
|
result = converter.send(:convert_array_to_body_params, [])
|
||||||
|
expect(result).to eq({})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts array to numbered body parameters' do
|
||||||
|
converter = described_class.new({}, template)
|
||||||
|
result = converter.send(:convert_array_to_body_params, %w[John Order123])
|
||||||
|
expect(result).to eq({ '1' => 'John', '2' => 'Order123' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#convert_hash_to_body_params' do
|
||||||
|
it 'converts hash to body parameters' do
|
||||||
|
converter = described_class.new({}, template)
|
||||||
|
result = converter.send(:convert_hash_to_body_params, { 'name' => 'John', 'order' => '123' })
|
||||||
|
expect(result).to eq({ 'name' => 'John', 'order' => '123' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -48,4 +48,4 @@ properties:
|
|||||||
type: object
|
type: object
|
||||||
description: The processed param values for template variables in template
|
description: The processed param values for template variables in template
|
||||||
example:
|
example:
|
||||||
1: 'Chatwoot'
|
1: 'Chatwoot'
|
||||||
Reference in New Issue
Block a user