diff --git a/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb b/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb index beb11d556..6e393f9f5 100644 --- a/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb +++ b/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb @@ -106,7 +106,10 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS policy: 'deterministic', code: template_info[:lang_code] }, - components: template_info[:parameters] + components: [{ + type: 'body', + parameters: template_info[:parameters] + }] } end diff --git a/app/services/whatsapp/providers/whatsapp_cloud_service.rb b/app/services/whatsapp/providers/whatsapp_cloud_service.rb index 34939048a..1620e4e42 100644 --- a/app/services/whatsapp/providers/whatsapp_cloud_service.rb +++ b/app/services/whatsapp/providers/whatsapp_cloud_service.rb @@ -12,20 +12,15 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi end 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( "#{phone_id_path}/messages", headers: api_headers, - body: request_body.to_json + body: { + messaging_product: 'whatsapp', + to: phone_number, + template: template_body_parameters(template_info), + type: 'template' + }.to_json ) process_response(response) @@ -124,36 +119,17 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi end def template_body_parameters(template_info) - template_body = { + { name: template_info[:name], language: { policy: 'deterministic', 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 def whatsapp_reply_context(message) diff --git a/app/services/whatsapp/template_converter_service.rb b/app/services/whatsapp/template_converter_service.rb deleted file mode 100644 index 6d858b3ce..000000000 --- a/app/services/whatsapp/template_converter_service.rb +++ /dev/null @@ -1,115 +0,0 @@ -# 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::TemplateConverterService - 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) - - # 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['format_version'] = 'enhanced' - - @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 diff --git a/app/services/whatsapp/template_parameter_service.rb b/app/services/whatsapp/template_parameter_service.rb deleted file mode 100644 index fb321900e..000000000 --- a/app/services/whatsapp/template_parameter_service.rb +++ /dev/null @@ -1,148 +0,0 @@ -class Whatsapp::TemplateParameterService - 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 diff --git a/app/services/whatsapp/template_processor_service.rb b/app/services/whatsapp/template_processor_service.rb index 1db97e8ea..2ce9fcf8f 100644 --- a/app/services/whatsapp/template_processor_service.rb +++ b/app/services/whatsapp/template_processor_service.rb @@ -2,9 +2,11 @@ class Whatsapp::TemplateProcessorService pattr_initialize [:channel!, :template_params, :message] def call - return [nil, nil, nil, nil] if template_params.blank? - - process_template_with_params + if template_params.present? + process_template_with_params + else + process_template_from_message + end end private @@ -18,6 +20,51 @@ class Whatsapp::TemplateProcessorService ] 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 channel.message_templates.find do |t| t['name'] == template_params['name'] && t['language'] == template_params['language'] && t['status']&.downcase == 'approved' @@ -28,100 +75,21 @@ class Whatsapp::TemplateProcessorService template = find_template return if template.blank? - # Convert legacy format to enhanced format before processing - converter = Whatsapp::TemplateConverterService.new(template_params, template) - normalized_params = converter.normalize_to_enhanced + parameter_format = template['parameter_format'] - 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)) - 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) - 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 + if parameter_format == 'NAMED' + 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 - header_params end - def media_url_with_type?(key, header_data) - key == 'media_url' && header_data['media_type'].present? - end + def validated_body_object(template) + # we don't care if its not approved template + return if template['status'] != 'approved' - 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::TemplateParameterService.new + # we only care about text body object in template. if not present we discard the template + # we don't support other forms of templates + template['components'].find { |obj| obj['type'] == 'BODY' && obj.key?('text') } end end diff --git a/spec/services/whatsapp/oneoff_campaign_service_spec.rb b/spec/services/whatsapp/oneoff_campaign_service_spec.rb index 1f542d631..33107e8de 100644 --- a/spec/services/whatsapp/oneoff_campaign_service_spec.rb +++ b/spec/services/whatsapp/oneoff_campaign_service_spec.rb @@ -19,7 +19,7 @@ describe Whatsapp::OneoffCampaignService do 'namespace' => '23423423_2342423_324234234_2343224', 'category' => 'UTILITY', 'language' => 'en', - 'processed_params' => { 'body' => { 'name' => 'John', 'ticket_id' => '2332' } } + 'processed_params' => { 'name' => 'John', 'ticket_id' => '2332' } } end @@ -125,13 +125,8 @@ describe Whatsapp::OneoffCampaignService do namespace: '23423423_2342423_324234234_2343224', lang_code: 'en', parameters: array_including( - hash_including( - type: 'body', - parameters: array_including( - hash_including(type: 'text', parameter_name: 'name', text: 'John'), - hash_including(type: 'text', parameter_name: 'ticket_id', text: '2332') - ) - ) + hash_including(type: 'text', parameter_name: 'name', text: 'John'), + hash_including(type: 'text', parameter_name: 'ticket_id', text: '2332') ) ) ) diff --git a/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb index 8735ccfbb..1bef635b3 100644 --- a/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb @@ -165,17 +165,19 @@ describe Whatsapp::Providers::WhatsappCloudService do let(:template_body) do { messaging_product: 'whatsapp', - recipient_type: 'individual', # Added recipient_type field to: '+123456789', - type: 'template', template: { name: template_info[:name], language: { policy: 'deterministic', code: template_info[:lang_code] }, - components: template_info[:parameters] # Changed to use parameters directly (enhanced format) - } + components: [ + { type: 'body', + parameters: template_info[:parameters] } + ] + }, + type: 'template' } end diff --git a/spec/services/whatsapp/send_on_whatsapp_service_spec.rb b/spec/services/whatsapp/send_on_whatsapp_service_spec.rb index e6b699a52..a0e3e893a 100644 --- a/spec/services/whatsapp/send_on_whatsapp_service_spec.rb +++ b/spec/services/whatsapp/send_on_whatsapp_service_spec.rb @@ -6,7 +6,7 @@ describe Whatsapp::SendOnWhatsappService do namespace: '23423423_2342423_324234234_2343224', language: 'en_US', category: 'Marketing', - processed_params: { 'body' => { '1' => '3' } } + processed_params: { '1' => '3' } } describe '#perform' do @@ -39,16 +39,15 @@ describe Whatsapp::SendOnWhatsappService do let(:named_template_body) do { messaging_product: 'whatsapp', - recipient_type: 'individual', to: '123456789', - type: 'template', template: { name: 'ticket_status_updated', language: { 'policy': 'deterministic', 'code': 'en_US' }, components: [{ 'type': 'body', 'parameters': [{ 'type': 'text', parameter_name: 'last_name', 'text': 'Dale' }, { 'type': 'text', parameter_name: 'ticket_id', 'text': '2332' }] }] - } + }, + type: 'template' } end @@ -74,7 +73,7 @@ describe Whatsapp::SendOnWhatsappService 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.', - conversation: conversation, additional_attributes: { template_params: template_params }) + conversation: conversation) stub_request(:post, 'https://waba.360dialog.io/v1/messages') .with( @@ -108,18 +107,12 @@ describe Whatsapp::SendOnWhatsappService do name: 'ticket_status_updated', language: 'en_US', category: 'UTILITY', - processed_params: { 'body' => { 'last_name' => 'Dale', 'ticket_id' => '2332' } } + processed_params: { 'last_name' => 'Dale', 'ticket_id' => '2332' } } stub_request(:post, "https://graph.facebook.com/v13.0/#{whatsapp_cloud_channel.provider_config['phone_number_id']}/messages") .with( - :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' - }, + :headers => { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{whatsapp_cloud_channel.provider_config['api_key']}" }, :body => named_template_body.to_json ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' }) message = create(:message, @@ -131,225 +124,30 @@ describe Whatsapp::SendOnWhatsappService do end it 'calls channel.send_template when template has regexp characters' do - regexp_template_params = build_template_params('customer_yes_no', '2342384942_32423423_23423fdsdaf23', 'ar', {}) - arabic_content = 'عميلنا العزيز الرجاء الرد على هذه الرسالة بكلمة *نعم* للرد على إستفساركم من قبل خدمة العملاء.' - message = create_message_with_template(arabic_content, regexp_template_params) - stub_template_request(regexp_template_params, []) - - 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') - .with( - headers: headers, - body: { - to: '123456789', - template: { - name: 'sample_shipping_confirmation', - namespace: '23423423_2342423_324234234_2343224', - language: { 'policy': 'deterministic', 'code': 'en_US' }, - components: [] - }, - type: 'template' - }.to_json - ).to_return(status: 200, body: success_response, headers: { 'content-type' => 'application/json' }) - - described_class.new(message: message).perform - expect(message.reload.source_id).to eq('123456789') - 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 + message = create( + :message, + message_type: :outgoing, + content: 'عميلنا العزيز الرجاء الرد على هذه الرسالة بكلمة *نعم* للرد على إستفساركم من قبل خدمة العملاء.', + conversation: conversation ) - 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 + name: 'customer_yes_no', + namespace: '2342384942_32423423_23423fdsdaf23', + language: { 'policy': 'deterministic', 'code': 'ar' }, + components: [{ 'type': 'body', 'parameters': [] }] }, 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' }) + described_class.new(message: message).perform + expect(message.reload.source_id).to eq('123456789') end end end diff --git a/spec/services/whatsapp/template_converter_service_spec.rb b/spec/services/whatsapp/template_converter_service_spec.rb deleted file mode 100644 index a79d94ce9..000000000 --- a/spec/services/whatsapp/template_converter_service_spec.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'rails_helper' - -describe Whatsapp::TemplateConverterService 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('enhanced') - 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('enhanced') - 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('enhanced') - 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